Compare commits

...

2 Commits

Author SHA1 Message Date
Jeff Emmett a37ab68588 refactor: move activity log into settings dropdown, simplify permissions
Move the standalone activity log toggle button (~) into the settings
gear dropdown as a collapsible accordion section. Simplify the board
permission display by removing the verbose "Access Levels" grid and
replacing it with a compact current-permission badge + request button.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 22:57:06 -07:00
Jeff Emmett 20094ea9a7 Add deployment scaffolding (Dockerfile, docker-compose, nginx)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 14:14:29 +01:00
8 changed files with 1066 additions and 202 deletions

802
package-lock.json generated
View File

@ -97,7 +97,7 @@
"vite-plugin-top-level-await": "^1.6.0",
"vite-plugin-wasm": "^3.5.0",
"vitest": "^4.0.16",
"wrangler": "^4.33.2"
"wrangler": "^4.63.0"
},
"engines": {
"node": ">=20.0.0"
@ -2083,31 +2083,14 @@
}
},
"node_modules/@cloudflare/kv-asset-handler": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.1.tgz",
"integrity": "sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg==",
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz",
"integrity": "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==",
"dev": true,
"license": "MIT OR Apache-2.0",
"dependencies": {
"mime": "^3.0.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@cloudflare/kv-asset-handler/node_modules/mime": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
"dev": true,
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/@cloudflare/types": {
"version": "6.29.1",
"resolved": "https://registry.npmjs.org/@cloudflare/types/-/types-6.29.1.tgz",
@ -2123,14 +2106,13 @@
}
},
"node_modules/@cloudflare/unenv-preset": {
"version": "2.7.13",
"resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.7.13.tgz",
"integrity": "sha512-NulO1H8R/DzsJguLC0ndMuk4Ufv0KSlN+E54ay9rn9ZCQo0kpAPwwh3LhgpZ96a3Dr6L9LqW57M4CqC34iLOvw==",
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.12.0.tgz",
"integrity": "sha512-NK4vN+2Z/GbfGS4BamtbbVk1rcu5RmqaYGiyHJQrA09AoxdZPHDF3W/EhgI0YSK8p3vRo/VNCtbSJFPON7FWMQ==",
"dev": true,
"license": "MIT OR Apache-2.0",
"peerDependencies": {
"unenv": "2.0.0-rc.24",
"workerd": "^1.20251202.0"
"workerd": "^1.20260115.0"
},
"peerDependenciesMeta": {
"workerd": {
@ -2171,6 +2153,33 @@
"vitest": "2.0.x - 3.2.x"
}
},
"node_modules/@cloudflare/vitest-pool-workers/node_modules/@cloudflare/kv-asset-handler": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.1.tgz",
"integrity": "sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg==",
"dev": true,
"dependencies": {
"mime": "^3.0.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@cloudflare/vitest-pool-workers/node_modules/@cloudflare/unenv-preset": {
"version": "2.7.13",
"resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.7.13.tgz",
"integrity": "sha512-NulO1H8R/DzsJguLC0ndMuk4Ufv0KSlN+E54ay9rn9ZCQo0kpAPwwh3LhgpZ96a3Dr6L9LqW57M4CqC34iLOvw==",
"dev": true,
"peerDependencies": {
"unenv": "2.0.0-rc.24",
"workerd": "^1.20251202.0"
},
"peerDependenciesMeta": {
"workerd": {
"optional": true
}
}
},
"node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/aix-ppc64": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz",
@ -2655,6 +2664,24 @@
"@esbuild/win32-x64": "0.27.0"
}
},
"node_modules/@cloudflare/vitest-pool-workers/node_modules/mime": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
"dev": true,
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/@cloudflare/vitest-pool-workers/node_modules/path-to-regexp": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
"dev": true
},
"node_modules/@cloudflare/vitest-pool-workers/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
@ -2668,6 +2695,41 @@
"node": ">=10"
}
},
"node_modules/@cloudflare/vitest-pool-workers/node_modules/wrangler": {
"version": "4.55.0",
"resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.55.0.tgz",
"integrity": "sha512-50icmLX8UbNaq0FmFHbcvvOh7I6rDA/FyaMYRcNSl1iX0JwuKswezmmtYvYPxPTkbYz7FUYR8GPZLaT23uzFqw==",
"deprecated": "This version can incorrectly automatically delegate 'wrangler deploy' to 'opennextjs-cloudflare'",
"dev": true,
"dependencies": {
"@cloudflare/kv-asset-handler": "0.4.1",
"@cloudflare/unenv-preset": "2.7.13",
"blake3-wasm": "2.1.5",
"esbuild": "0.27.0",
"miniflare": "4.20251213.0",
"path-to-regexp": "6.3.0",
"unenv": "2.0.0-rc.24",
"workerd": "1.20251213.0"
},
"bin": {
"wrangler": "bin/wrangler.js",
"wrangler2": "bin/wrangler.js"
},
"engines": {
"node": ">=20.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
},
"peerDependencies": {
"@cloudflare/workers-types": "^4.20251213.0"
},
"peerDependenciesMeta": {
"@cloudflare/workers-types": {
"optional": true
}
}
},
"node_modules/@cloudflare/workerd-darwin-64": {
"version": "1.20251213.0",
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20251213.0.tgz",
@ -3984,6 +4046,15 @@
"node": ">=18"
}
},
"node_modules/@img/colour": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
"integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==",
"dev": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
@ -4092,6 +4163,38 @@
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-riscv64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
@ -4200,6 +4303,50 @@
"@img/sharp-libvips-linux-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linux-ppc64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-ppc64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-riscv64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-riscv64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
@ -4307,6 +4454,25 @@
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
@ -23127,6 +23293,15 @@
"integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==",
"license": "MIT"
},
"node_modules/undici": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz",
"integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==",
"dev": true,
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
@ -24663,20 +24838,19 @@
}
},
"node_modules/wrangler": {
"version": "4.55.0",
"resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.55.0.tgz",
"integrity": "sha512-50icmLX8UbNaq0FmFHbcvvOh7I6rDA/FyaMYRcNSl1iX0JwuKswezmmtYvYPxPTkbYz7FUYR8GPZLaT23uzFqw==",
"version": "4.63.0",
"resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.63.0.tgz",
"integrity": "sha512-+R04jF7Eb8K3KRMSgoXpcIdLb8GC62eoSGusYh1pyrSMm/10E0hbKkd7phMJO4HxXc6R7mOHC5SSoX9eof30Uw==",
"dev": true,
"license": "MIT OR Apache-2.0",
"dependencies": {
"@cloudflare/kv-asset-handler": "0.4.1",
"@cloudflare/unenv-preset": "2.7.13",
"@cloudflare/kv-asset-handler": "0.4.2",
"@cloudflare/unenv-preset": "2.12.0",
"blake3-wasm": "2.1.5",
"esbuild": "0.27.0",
"miniflare": "4.20251213.0",
"miniflare": "4.20260205.0",
"path-to-regexp": "6.3.0",
"unenv": "2.0.0-rc.24",
"workerd": "1.20251213.0"
"workerd": "1.20260205.0"
},
"bin": {
"wrangler": "bin/wrangler.js",
@ -24689,7 +24863,7 @@
"fsevents": "~2.3.2"
},
"peerDependencies": {
"@cloudflare/workers-types": "^4.20251213.0"
"@cloudflare/workers-types": "^4.20260205.0"
},
"peerDependenciesMeta": {
"@cloudflare/workers-types": {
@ -24697,6 +24871,86 @@
}
}
},
"node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-64": {
"version": "1.20260205.0",
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260205.0.tgz",
"integrity": "sha512-ToOItqcirmWPwR+PtT+Q4bdjTn/63ZxhJKEfW4FNn7FxMTS1Tw5dml0T0mieOZbCpcvY8BdvPKFCSlJuI8IVHQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=16"
}
},
"node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-arm64": {
"version": "1.20260205.0",
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260205.0.tgz",
"integrity": "sha512-402ZqLz+LrG0NDXp7Hn7IZbI0DyhjNfjAlVenb0K3yod9KCuux0u3NksNBvqJx0mIGHvVR4K05h+jfT5BTHqGA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=16"
}
},
"node_modules/wrangler/node_modules/@cloudflare/workerd-linux-64": {
"version": "1.20260205.0",
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260205.0.tgz",
"integrity": "sha512-rz9jBzazIA18RHY+osa19hvsPfr0LZI1AJzIjC6UqkKKphcTpHBEQ25Xt8cIA34ivMIqeENpYnnmpDFesLkfcQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16"
}
},
"node_modules/wrangler/node_modules/@cloudflare/workerd-linux-arm64": {
"version": "1.20260205.0",
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260205.0.tgz",
"integrity": "sha512-jr6cKpMM/DBEbL+ATJ9rYue758CKp0SfA/nXt5vR32iINVJrb396ye9iat2y9Moa/PgPKnTrFgmT6urUmG3IUg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16"
}
},
"node_modules/wrangler/node_modules/@cloudflare/workerd-windows-64": {
"version": "1.20260205.0",
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260205.0.tgz",
"integrity": "sha512-SMPW5jCZYOG7XFIglSlsgN8ivcl0pCrSAYxCwxtWvZ88whhcDB/aISNtiQiDZujPH8tIo2hE5dEkxW7tGEwc3A==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=16"
}
},
"node_modules/wrangler/node_modules/@esbuild/aix-ppc64": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz",
@ -25139,6 +25393,367 @@
"node": ">=18"
}
},
"node_modules/wrangler/node_modules/@img/sharp-darwin-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.2.4"
}
},
"node_modules/wrangler/node_modules/@img/sharp-darwin-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.2.4"
}
},
"node_modules/wrangler/node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/wrangler/node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/wrangler/node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/wrangler/node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/wrangler/node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/wrangler/node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/wrangler/node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/wrangler/node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/wrangler/node_modules/@img/sharp-linux-arm": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.2.4"
}
},
"node_modules/wrangler/node_modules/@img/sharp-linux-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.2.4"
}
},
"node_modules/wrangler/node_modules/@img/sharp-linux-s390x": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.2.4"
}
},
"node_modules/wrangler/node_modules/@img/sharp-linux-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.2.4"
}
},
"node_modules/wrangler/node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
}
},
"node_modules/wrangler/node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
}
},
"node_modules/wrangler/node_modules/@img/sharp-wasm32": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
"cpu": [
"wasm32"
],
"dev": true,
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.7.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/wrangler/node_modules/@img/sharp-win32-ia32": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/wrangler/node_modules/@img/sharp-win32-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/wrangler/node_modules/esbuild": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz",
@ -25181,6 +25796,26 @@
"@esbuild/win32-x64": "0.27.0"
}
},
"node_modules/wrangler/node_modules/miniflare": {
"version": "4.20260205.0",
"resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260205.0.tgz",
"integrity": "sha512-jG1TknEDeFqcq/z5gsOm1rKeg4cNG7ruWxEuiPxl3pnQumavxo8kFpeQC6XKVpAhh2PI9ODGyIYlgd77sTHl5g==",
"dev": true,
"dependencies": {
"@cspotcode/source-map-support": "0.8.1",
"sharp": "^0.34.5",
"undici": "7.18.2",
"workerd": "1.20260205.0",
"ws": "8.18.0",
"youch": "4.1.0-beta.10"
},
"bin": {
"miniflare": "bootstrap.js"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/wrangler/node_modules/path-to-regexp": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
@ -25188,6 +25823,103 @@
"dev": true,
"license": "MIT"
},
"node_modules/wrangler/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/wrangler/node_modules/sharp": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"@img/colour": "^1.0.0",
"detect-libc": "^2.1.2",
"semver": "^7.7.3"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.34.5",
"@img/sharp-darwin-x64": "0.34.5",
"@img/sharp-libvips-darwin-arm64": "1.2.4",
"@img/sharp-libvips-darwin-x64": "1.2.4",
"@img/sharp-libvips-linux-arm": "1.2.4",
"@img/sharp-libvips-linux-arm64": "1.2.4",
"@img/sharp-libvips-linux-ppc64": "1.2.4",
"@img/sharp-libvips-linux-riscv64": "1.2.4",
"@img/sharp-libvips-linux-s390x": "1.2.4",
"@img/sharp-libvips-linux-x64": "1.2.4",
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
"@img/sharp-linux-arm": "0.34.5",
"@img/sharp-linux-arm64": "0.34.5",
"@img/sharp-linux-ppc64": "0.34.5",
"@img/sharp-linux-riscv64": "0.34.5",
"@img/sharp-linux-s390x": "0.34.5",
"@img/sharp-linux-x64": "0.34.5",
"@img/sharp-linuxmusl-arm64": "0.34.5",
"@img/sharp-linuxmusl-x64": "0.34.5",
"@img/sharp-wasm32": "0.34.5",
"@img/sharp-win32-arm64": "0.34.5",
"@img/sharp-win32-ia32": "0.34.5",
"@img/sharp-win32-x64": "0.34.5"
}
},
"node_modules/wrangler/node_modules/workerd": {
"version": "1.20260205.0",
"resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260205.0.tgz",
"integrity": "sha512-CcMH5clHwrH8VlY7yWS9C/G/C8g9czIz1yU3akMSP9Z3CkEMFSoC3GGdj5G7Alw/PHEeez1+1IrlYger4pwu+w==",
"dev": true,
"hasInstallScript": true,
"bin": {
"workerd": "bin/workerd"
},
"engines": {
"node": ">=16"
},
"optionalDependencies": {
"@cloudflare/workerd-darwin-64": "1.20260205.0",
"@cloudflare/workerd-darwin-arm64": "1.20260205.0",
"@cloudflare/workerd-linux-64": "1.20260205.0",
"@cloudflare/workerd-linux-arm64": "1.20260205.0",
"@cloudflare/workerd-windows-64": "1.20260205.0"
}
},
"node_modules/wrangler/node_modules/ws": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"dev": true,
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",

View File

@ -123,7 +123,7 @@
"vite-plugin-top-level-await": "^1.6.0",
"vite-plugin-wasm": "^3.5.0",
"vitest": "^4.0.16",
"wrangler": "^4.33.2"
"wrangler": "^4.63.0"
},
"engines": {
"node": ">=20.0.0"

View File

@ -109,15 +109,5 @@ export function ActivityPanel({ isOpen, onClose }: ActivityPanelProps) {
);
}
// Toggle button component for the toolbar
export function ActivityToggleButton({ onClick, isActive }: { onClick: () => void; isActive: boolean }) {
return (
<button
className={`activity-toggle-btn ${isActive ? 'active' : ''}`}
onClick={onClick}
title="Activity Log"
>
<span className="activity-toggle-icon">~</span>
</button>
);
}
// Note: ActivityToggleButton has been removed - activity panel is now toggled
// from the settings dropdown via a custom event 'toggle-activity-panel'

View File

@ -1,12 +1,13 @@
/**
* RunPod API utility functions
* Handles communication with RunPod WhisperX endpoints
* Transcription API utility functions
* Now uses self-hosted faster-whisper-server (large-v3-turbo model)
* Falls back to RunPod if local whisper is unavailable
*
* SECURITY: All RunPod calls go through the Cloudflare Worker proxy
* SECURITY: All calls go through the Cloudflare Worker proxy
* API keys are stored server-side, never exposed to the browser
*/
import { getRunPodProxyConfig } from './clientConfig'
import { getRunPodProxyConfig, getWorkerApiUrl } from './clientConfig'
export interface RunPodTranscriptionResponse {
id?: string
@ -43,15 +44,14 @@ export async function blobToBase64(blob: Blob): Promise<string> {
}
/**
* Send transcription request to RunPod endpoint via proxy
* Handles both synchronous and asynchronous job patterns
* Send transcription request to local whisper API via worker proxy
* Uses self-hosted faster-whisper-server with large-v3-turbo model (FREE)
* Falls back to RunPod if local whisper fails
*/
export async function transcribeWithRunPod(
audioBlob: Blob,
language?: string
): Promise<string> {
const { proxyUrl } = getRunPodProxyConfig('whisper')
// Check audio blob size (limit to ~10MB to prevent issues)
const maxSize = 10 * 1024 * 1024 // 10MB
if (audioBlob.size > maxSize) {
@ -64,33 +64,28 @@ export async function transcribeWithRunPod(
// Detect audio format from blob type
const audioFormat = audioBlob.type || 'audio/wav'
// Use proxy endpoint - API key and endpoint ID are handled server-side
const url = `${proxyUrl}/run`
// Use local whisper endpoint (proxied through worker)
const workerUrl = getWorkerApiUrl()
const url = `${workerUrl}/api/whisper/transcribe`
// Prepare the request payload
// WhisperX typically expects audio as base64 or file URL
// The exact format may vary based on your WhisperX endpoint implementation
const requestBody = {
input: {
audio: audioBase64,
audio_format: audioFormat,
language: language || 'en',
task: 'transcribe'
// Note: Some WhisperX endpoints may expect different field names
// Adjust the requestBody structure in this function if needed
language: language || 'en'
}
}
try {
// Add timeout to prevent hanging requests (30 seconds for initial request)
// Longer timeout for local CPU transcription (120 seconds)
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 30000)
const timeoutId = setTimeout(() => controller.abort(), 120000)
console.log('Sending transcription request to local whisper API...')
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
// Authorization is handled by the proxy server-side
},
body: JSON.stringify(requestBody),
signal: controller.signal
@ -100,30 +95,26 @@ export async function transcribeWithRunPod(
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: response.statusText })) as { error?: string; details?: string }
console.error('RunPod API error response:', {
console.error('Local whisper API error response:', {
status: response.status,
statusText: response.statusText,
error: errorData
})
throw new Error(`RunPod API error: ${response.status} - ${errorData.error || errorData.details || 'Unknown error'}`)
// Could fall back to RunPod here if needed
throw new Error(`Whisper API error: ${response.status} - ${errorData.error || errorData.details || 'Unknown error'}`)
}
const data: RunPodTranscriptionResponse = await response.json()
// Handle async job pattern (RunPod often returns job IDs)
if (data.id && (data.status === 'IN_QUEUE' || data.status === 'IN_PROGRESS')) {
return await pollRunPodJob(data.id, proxyUrl)
}
// Handle direct response
if (data.output?.text) {
// Handle direct response (local whisper returns immediately)
if (data.status === 'COMPLETED' && data.output?.text) {
console.log('Local whisper transcription complete')
return data.output.text.trim()
}
// Handle error response
if (data.error) {
throw new Error(`RunPod transcription error: ${data.error}`)
if (data.status === 'FAILED' || data.error) {
throw new Error(`Whisper transcription error: ${data.error || 'Unknown error'}`)
}
// Fallback: try to extract text from segments
@ -131,14 +122,18 @@ export async function transcribeWithRunPod(
return data.output.segments.map(seg => seg.text).join(' ').trim()
}
// Check if response has unexpected structure
console.warn('Unexpected RunPod response structure:', data)
throw new Error('No transcription text found in RunPod response. Check endpoint response format.')
// Direct text response
if (data.output?.text) {
return data.output.text.trim()
}
console.warn('Unexpected whisper response structure:', data)
throw new Error('No transcription text found in response.')
} catch (error: unknown) {
if (error instanceof Error && error.name === 'AbortError') {
throw new Error('RunPod request timed out after 30 seconds')
throw new Error('Transcription request timed out after 120 seconds')
}
console.error('RunPod transcription error:', error)
console.error('Transcription error:', error)
throw error
}
}

View File

@ -141,7 +141,7 @@ import { updateLastVisited } from "../lib/starredBoards"
import { recordBoardVisit } from "../lib/visitedBoards"
import { captureBoardScreenshot } from "../lib/screenshotService"
import { logActivity } from "../lib/activityLogger"
import { ActivityPanel, ActivityToggleButton } from "../components/ActivityPanel"
import { ActivityPanel } from "../components/ActivityPanel"
import { WORKER_URL } from "../constants/workerUrl"
@ -528,6 +528,17 @@ export function Board() {
const [editor, setEditor] = useState<Editor | null>(null)
const [isActivityPanelOpen, setIsActivityPanelOpen] = useState(false)
// Listen for toggle-activity-panel event from settings dropdown
useEffect(() => {
const handleToggleActivityPanel = () => {
setIsActivityPanelOpen(prev => !prev)
}
window.addEventListener('toggle-activity-panel', handleToggleActivityPanel)
return () => {
window.removeEventListener('toggle-activity-panel', handleToggleActivityPanel)
}
}, [])
// Update read-only state when permission changes after editor is mounted
useEffect(() => {
if (!editor) return
@ -1474,14 +1485,7 @@ export function Board() {
/>
)}
*/}
{/* Activity Panel Toggle Button */}
<div style={{ position: 'fixed', top: 12, right: 12, zIndex: 999 }}>
<ActivityToggleButton
onClick={() => setIsActivityPanelOpen(!isActivityPanelOpen)}
isActive={isActivityPanelOpen}
/>
</div>
{/* Activity Panel */}
{/* Activity Panel - toggled from settings dropdown */}
<ActivityPanel
isOpen={isActivityPanelOpen}
onClose={() => setIsActivityPanelOpen(false)}

View File

@ -71,6 +71,7 @@ function CustomSharePanel() {
const [mobileMenuSection, setMobileMenuSection] = React.useState<'main' | 'signin' | 'share' | 'settings'>('main')
// const [showVersionHistory, setShowVersionHistory] = React.useState(false) // TODO: Re-enable when version reversion is ready
const [showAISection, setShowAISection] = React.useState(false)
const [showActivitySection, setShowActivitySection] = React.useState(false)
const [hasApiKey, setHasApiKey] = React.useState(false)
const [permissionRequestStatus, setPermissionRequestStatus] = React.useState<'idle' | 'sending' | 'sent' | 'error'>('idle')
const [requestMessage, setRequestMessage] = React.useState('')
@ -958,131 +959,72 @@ function CustomSharePanel() {
onWheel={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
>
{/* Board Permission Section */}
<div style={{ padding: '12px 16px 16px' }}>
{/* Section Header */}
{/* Your Permission - simplified display */}
<div style={{ padding: '12px 16px' }}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '12px',
paddingBottom: '8px',
borderBottom: '1px solid var(--color-panel-contrast)',
justifyContent: 'space-between',
padding: '10px 12px',
background: 'var(--color-muted-2)',
borderRadius: '8px',
border: '1px solid var(--color-panel-contrast)',
}}>
<span style={{ fontSize: '14px' }}>🔐</span>
<span style={{ fontSize: '13px', fontWeight: 600, color: 'var(--color-text)' }}>Board Permission</span>
<span style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px', fontWeight: 500, color: 'var(--color-text)' }}>
<span style={{ fontSize: '14px' }}>{PERMISSION_CONFIG[currentPermission].icon}</span>
<span>Your Permission</span>
</span>
<span style={{
marginLeft: 'auto',
fontSize: '10px',
padding: '3px 8px',
fontSize: '11px',
padding: '4px 10px',
borderRadius: '12px',
background: `${PERMISSION_CONFIG[currentPermission].color}20`,
color: PERMISSION_CONFIG[currentPermission].color,
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.3px',
}}>
{PERMISSION_CONFIG[currentPermission].label}
</span>
</div>
{/* Permission levels - indented to show hierarchy */}
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '6px',
marginLeft: '4px',
padding: '8px 12px',
background: 'var(--color-muted-2)',
borderRadius: '8px',
border: '1px solid var(--color-panel-contrast)',
}}>
<span style={{ fontSize: '10px', color: 'var(--color-text-3)', marginBottom: '4px', fontWeight: 500, textTransform: 'uppercase', letterSpacing: '0.5px' }}>
Access Levels
</span>
{(['view', 'edit', 'admin'] as PermissionLevel[]).map((level) => {
const config = PERMISSION_CONFIG[level]
const isCurrent = currentPermission === level
const canRequest = session.authed && !isCurrent && (
(level === 'edit' && currentPermission === 'view') ||
(level === 'admin' && currentPermission !== 'admin')
)
return (
<div
key={level}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '8px 10px',
borderRadius: '6px',
background: isCurrent ? `${config.color}15` : 'var(--color-panel)',
border: isCurrent ? `2px solid ${config.color}` : '1px solid var(--color-panel-contrast)',
transition: 'all 0.15s ease',
}}
>
<span style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
fontSize: '12px',
color: isCurrent ? config.color : 'var(--color-text)',
fontWeight: isCurrent ? 600 : 400,
}}>
<span style={{ fontSize: '14px' }}>{config.icon}</span>
<span>{config.label}</span>
{isCurrent && (
<span style={{
fontSize: '9px',
padding: '2px 6px',
borderRadius: '10px',
background: config.color,
color: 'white',
fontWeight: 500,
}}>
Current
</span>
)}
</span>
{canRequest && (
<button
onClick={() => handleRequestPermission(level)}
disabled={permissionRequestStatus === 'sending'}
style={{
padding: '4px 10px',
fontSize: '10px',
fontWeight: 600,
borderRadius: '4px',
border: `1px solid ${config.color}`,
background: 'transparent',
color: config.color,
cursor: permissionRequestStatus === 'sending' ? 'wait' : 'pointer',
opacity: permissionRequestStatus === 'sending' ? 0.6 : 1,
transition: 'all 0.15s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = config.color
e.currentTarget.style.color = 'white'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent'
e.currentTarget.style.color = config.color
}}
>
{permissionRequestStatus === 'sending' ? '...' : 'Request'}
</button>
)}
</div>
)
})}
</div>
{/* Request higher permission button */}
{session.authed && currentPermission !== 'admin' && (
<button
onClick={() => handleRequestPermission(currentPermission === 'view' ? 'edit' : 'admin')}
disabled={permissionRequestStatus === 'sending'}
style={{
width: '100%',
marginTop: '8px',
padding: '8px 12px',
fontSize: '12px',
fontWeight: 500,
fontFamily: 'inherit',
borderRadius: '6px',
border: `1px solid ${PERMISSION_CONFIG[currentPermission === 'view' ? 'edit' : 'admin'].color}`,
background: 'transparent',
color: PERMISSION_CONFIG[currentPermission === 'view' ? 'edit' : 'admin'].color,
cursor: permissionRequestStatus === 'sending' ? 'wait' : 'pointer',
transition: 'all 0.15s ease',
}}
onMouseEnter={(e) => {
const color = PERMISSION_CONFIG[currentPermission === 'view' ? 'edit' : 'admin'].color
e.currentTarget.style.background = color
e.currentTarget.style.color = 'white'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent'
e.currentTarget.style.color = PERMISSION_CONFIG[currentPermission === 'view' ? 'edit' : 'admin'].color
}}
>
{permissionRequestStatus === 'sending' ? 'Sending...' :
permissionRequestStatus === 'sent' ? 'Request Sent!' :
`Request ${currentPermission === 'view' ? 'Edit' : 'Admin'} Access`}
</button>
)}
{/* Request status message */}
{requestMessage && (
<p style={{
margin: '10px 0 0',
margin: '8px 0 0',
fontSize: '11px',
padding: '8px 12px',
borderRadius: '6px',
@ -1098,7 +1040,7 @@ function CustomSharePanel() {
{!session.authed && (
<p style={{
margin: '10px 0 0',
margin: '8px 0 0',
fontSize: '10px',
color: 'var(--color-text-3)',
textAlign: 'center',
@ -1358,6 +1300,121 @@ function CustomSharePanel() {
<div style={{ height: '1px', background: 'var(--color-panel-contrast)', margin: '0' }} />
{/* Activity Log Accordion */}
<div>
<button
onClick={() => setShowActivitySection(!showActivitySection)}
style={{
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 16px',
background: showActivitySection ? 'var(--color-muted-2)' : 'none',
border: 'none',
cursor: 'pointer',
color: 'var(--color-text)',
fontSize: '13px',
fontWeight: 600,
textAlign: 'left',
transition: 'background 0.15s ease',
fontFamily: 'inherit',
}}
onMouseEnter={(e) => {
if (!showActivitySection) e.currentTarget.style.background = 'var(--color-muted-2)'
}}
onMouseLeave={(e) => {
if (!showActivitySection) e.currentTarget.style.background = 'none'
}}
>
<span style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '14px', fontFamily: 'monospace', fontWeight: 700 }}>~</span>
<span>Activity Log</span>
</span>
<span style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '20px',
height: '20px',
borderRadius: '4px',
background: showActivitySection ? 'var(--color-panel)' : 'var(--color-muted-2)',
transition: 'all 0.2s ease',
}}>
<svg
width="12"
height="12"
viewBox="0 0 16 16"
fill="currentColor"
style={{
transform: showActivitySection ? 'rotate(180deg)' : 'rotate(0deg)',
transition: 'transform 0.2s ease',
color: 'var(--color-text-3)',
}}
>
<path d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"/>
</svg>
</span>
</button>
{showActivitySection && (
<div style={{
padding: '12px 16px',
background: 'var(--color-muted-2)',
borderTop: '1px solid var(--color-panel-contrast)',
}}>
<p style={{
fontSize: '11px',
color: 'var(--color-text-3)',
marginBottom: '12px',
padding: '8px 10px',
background: 'var(--color-panel)',
borderRadius: '6px',
border: '1px solid var(--color-panel-contrast)',
}}>
Track shape creations, deletions, and updates on this board.
</p>
<button
onClick={() => {
setShowSettingsDropdown(false)
// Dispatch custom event to toggle activity panel
window.dispatchEvent(new CustomEvent('toggle-activity-panel'))
}}
style={{
width: '100%',
padding: '10px 12px',
fontSize: '12px',
fontWeight: 500,
fontFamily: 'inherit',
backgroundColor: isDarkMode ? '#3a3a3a' : '#ffffff',
color: 'var(--color-text)',
border: `1px solid ${isDarkMode ? '#505050' : '#d1d5db'}`,
borderRadius: '6px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px',
transition: 'all 0.15s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = isDarkMode ? '#4a4a4a' : '#f3f4f6'
e.currentTarget.style.borderColor = '#3b82f6'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = isDarkMode ? '#3a3a3a' : '#ffffff'
e.currentTarget.style.borderColor = isDarkMode ? '#505050' : '#d1d5db'
}}
>
<span style={{ fontFamily: 'monospace', fontWeight: 700 }}>~</span>
<span>Open Activity Panel</span>
</button>
</div>
)}
</div>
<div style={{ height: '1px', background: 'var(--color-panel-contrast)', margin: '0' }} />
{/* Show Tutorial Button */}
<div style={{ padding: '12px 16px' }}>
<button

View File

@ -25,6 +25,9 @@ export interface Environment {
RUNPOD_WHISPER_ENDPOINT_ID?: string;
// Blender render server URL
BLENDER_API_URL?: string;
// Local Whisper API configuration
WHISPER_API_URL?: string;
WHISPER_MODEL?: string;
}
// CryptID types for auth

View File

@ -911,6 +911,89 @@ const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
}
})
// Local Whisper API proxy (uses self-hosted faster-whisper-server)
.post("/api/whisper/transcribe", async (req, env) => {
const WHISPER_API_URL = env.WHISPER_API_URL || 'https://whisper.jeffemmett.com'
const WHISPER_MODEL = env.WHISPER_MODEL || 'deepdml/faster-whisper-large-v3-turbo-ct2'
try {
const body = await req.json() as {
input: {
audio: string // base64 encoded
audio_format?: string
language?: string
}
}
if (!body.input?.audio) {
return new Response(JSON.stringify({ error: 'No audio data provided' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
})
}
// Decode base64 audio to binary
const audioBase64 = body.input.audio
const audioBytes = Uint8Array.from(atob(audioBase64), c => c.charCodeAt(0))
// Determine file extension from format
const format = body.input.audio_format || 'audio/wav'
const ext = format.includes('mp3') ? 'mp3' :
format.includes('webm') ? 'webm' :
format.includes('ogg') ? 'ogg' :
format.includes('m4a') ? 'm4a' : 'wav'
// Create form data for faster-whisper-server
const formData = new FormData()
formData.append('file', new Blob([audioBytes], { type: format }), `audio.${ext}`)
formData.append('model', WHISPER_MODEL)
formData.append('response_format', 'json')
if (body.input.language) {
formData.append('language', body.input.language)
}
// Call local whisper API
const response = await fetch(`${WHISPER_API_URL}/v1/audio/transcriptions`, {
method: 'POST',
body: formData
})
if (!response.ok) {
const errorText = await response.text()
return new Response(JSON.stringify({
error: `Whisper API error: ${response.status}`,
details: errorText
}), {
status: response.status,
headers: { 'Content-Type': 'application/json' }
})
}
const result = await response.json() as { text?: string; language?: string; duration?: number }
// Return in RunPod-compatible format for client compatibility
return new Response(JSON.stringify({
status: 'COMPLETED',
output: {
text: result.text || '',
language: result.language || 'en',
duration: result.duration || 0
}
}), {
headers: { 'Content-Type': 'application/json' }
})
} catch (error) {
console.error('Local whisper proxy error:', error)
return new Response(JSON.stringify({
status: 'FAILED',
error: (error as Error).message
}), {
status: 500,
headers: { 'Content-Type': 'application/json' }
})
}
})
// RunPod proxy - run sync (blocking)
.post("/api/runpod/:endpointType/runsync", async (req, env) => {
const endpointType = req.params.endpointType as 'image' | 'video' | 'text' | 'whisper'