diff --git a/screenshots/en/5.png b/screenshots/en/5.png new file mode 100644 index 0000000..e5b7a04 Binary files /dev/null and b/screenshots/en/5.png differ diff --git a/scripts/cloudflare-email-worker/package.json b/scripts/cloudflare-email-worker/package.json new file mode 100644 index 0000000..18ac47f --- /dev/null +++ b/scripts/cloudflare-email-worker/package.json @@ -0,0 +1,17 @@ +{ + "name": "agent-desk-email-inbound-worker", + "version": "1.0.0", + "description": "Cloudflare Email Routing Worker forwarding inbound emails to the Agent Desk email webhook", + "main": "src/index.ts", + "scripts": { + "deploy": "wrangler deploy" + }, + "dependencies": { + "postal-mime": "^2.1.8" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20241022.0", + "typescript": "^6.0.3", + "wrangler": "^3.84.1" + } +} diff --git a/scripts/cloudflare-email-worker/pnpm-lock.yaml b/scripts/cloudflare-email-worker/pnpm-lock.yaml new file mode 100644 index 0000000..7ab00b2 --- /dev/null +++ b/scripts/cloudflare-email-worker/pnpm-lock.yaml @@ -0,0 +1,984 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + postal-mime: + specifier: ^2.1.8 + version: 2.7.6 + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.20241022.0 + version: 4.20260702.1 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + wrangler: + specifier: ^3.84.1 + version: 3.114.17(@cloudflare/workers-types@4.20260702.1) + +packages: + + '@cloudflare/kv-asset-handler@0.3.4': + resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==} + engines: {node: '>=16.13'} + + '@cloudflare/unenv-preset@2.0.2': + resolution: {integrity: sha512-nyzYnlZjjV5xT3LizahG1Iu6mnrCaxglJ04rZLpDwlDVDZ7v46lNsfxhV3A/xtfgQuSHmLnc6SVI+KwBpc3Lwg==} + peerDependencies: + unenv: 2.0.0-rc.14 + workerd: ^1.20250124.0 + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20250718.0': + resolution: {integrity: sha512-FHf4t7zbVN8yyXgQ/r/GqLPaYZSGUVzeR7RnL28Mwj2djyw2ZergvytVc7fdGcczl6PQh+VKGfZCfUqpJlbi9g==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20250718.0': + resolution: {integrity: sha512-fUiyUJYyqqp4NqJ0YgGtp4WJh/II/YZsUnEb6vVy5Oeas8lUOxnN+ZOJ8N/6/5LQCVAtYCChRiIrBbfhTn5Z8Q==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20250718.0': + resolution: {integrity: sha512-5+eb3rtJMiEwp08Kryqzzu8d1rUcK+gdE442auo5eniMpT170Dz0QxBrqkg2Z48SFUPYbj+6uknuA5tzdRSUSg==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20250718.0': + resolution: {integrity: sha512-Aa2M/DVBEBQDdATMbn217zCSFKE+ud/teS+fFS+OQqKABLn0azO2qq6ANAHYOIE6Q3Sq4CxDIQr8lGdaJHwUog==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20250718.0': + resolution: {integrity: sha512-dY16RXKffmugnc67LTbyjdDHZn5NoTF1yHEf2fN4+OaOnoGSp3N1x77QubTDwqZ9zECWxgQfDLjddcH8dWeFhg==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cloudflare/workers-types@4.20260702.1': + resolution: {integrity: sha512-mOhf5TUEB1m2vPrxtqoIGfz0fUC9xyxRDx5gWHy5s+OCo6dcV+g7wI1R7gYCMFohhqF/2y2xeKVwMwCJjfn/WA==} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + + '@esbuild-plugins/node-globals-polyfill@0.2.3': + resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==} + peerDependencies: + esbuild: '*' + + '@esbuild-plugins/node-modules-polyfill@0.2.2': + resolution: {integrity: sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==} + peerDependencies: + esbuild: '*' + + '@esbuild/android-arm64@0.17.19': + resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.17.19': + resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.17.19': + resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.17.19': + resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.17.19': + resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.17.19': + resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.17.19': + resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.17.19': + resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.17.19': + resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.17.19': + resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.17.19': + resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.17.19': + resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.17.19': + resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.17.19': + resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.17.19': + resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.17.19': + resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.17.19': + resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.17.19': + resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.17.19': + resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.17.19': + resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.17.19': + resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.17.19': + resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@fastify/busboy@2.1.1': + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + acorn-walk@8.3.2: + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + engines: {node: '>=0.4.0'} + + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + + as-table@1.0.55: + resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} + + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + data-uri-to-buffer@2.0.2: + resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + esbuild@0.17.19: + resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} + engines: {node: '>=12'} + hasBin: true + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + estree-walker@0.6.1: + resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} + + exit-hook@2.2.1: + resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} + engines: {node: '>=6'} + + exsolve@1.1.1: + resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-source@2.0.12: + resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + is-arrayish@0.3.4: + resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + + magic-string@0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + + miniflare@3.20250718.3: + resolution: {integrity: sha512-JuPrDJhwLrNLEJiNLWO7ZzJrv/Vv9kZuwMYCfv0LskQDM6Eonw4OvywO3CH/wCGjgHzha/qyjUh8JQ068TjDgQ==} + engines: {node: '>=16.13'} + hasBin: true + + mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + + ohash@2.0.12: + resolution: {integrity: sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + postal-mime@2.7.6: + resolution: {integrity: sha512-UUlyE2KlxmvwMGq060onF/VWU3zD6dVW31FwokQ5v26jWd35fbs2MoTFzh+jXnPwAyV2MULMKXcBInGOl5TO6Q==} + + printable-characters@1.0.42: + resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} + + rollup-plugin-inject@3.0.2: + resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==} + deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject. + + rollup-plugin-node-polyfills@0.2.1: + resolution: {integrity: sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==} + + rollup-pluginutils@2.8.2: + resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + simple-swizzle@0.2.4: + resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + sourcemap-codec@1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead + + stacktracey@2.2.0: + resolution: {integrity: sha512-ETyQEz+CzXiLjEbyJqpbp+/T79RQD/6wqFucRBIlVNZfYq2Ay7wbretD4cxpbymZlaPWx58aIhPEY1Cr8DlVvg==} + + stoppable@1.1.0: + resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} + engines: {node: '>=4', npm: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + undici@5.29.0: + resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} + engines: {node: '>=14.0'} + + unenv@2.0.0-rc.14: + resolution: {integrity: sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q==} + + workerd@1.20250718.0: + resolution: {integrity: sha512-kqkIJP/eOfDlUyBzU7joBg+tl8aB25gEAGqDap+nFWb+WHhnooxjGHgxPBy3ipw2hnShPFNOQt5lFRxbwALirg==} + engines: {node: '>=16'} + hasBin: true + + wrangler@3.114.17: + resolution: {integrity: sha512-tAvf7ly+tB+zwwrmjsCyJ2pJnnc7SZhbnNwXbH+OIdVas3zTSmjcZOjmLKcGGptssAA3RyTKhcF9BvKZzMUycA==} + engines: {node: '>=16.17.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20250408.0 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + 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 + + youch@3.3.4: + resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==} + + zod@3.22.3: + resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} + +snapshots: + + '@cloudflare/kv-asset-handler@0.3.4': + dependencies: + mime: 3.0.0 + + '@cloudflare/unenv-preset@2.0.2(unenv@2.0.0-rc.14)(workerd@1.20250718.0)': + dependencies: + unenv: 2.0.0-rc.14 + optionalDependencies: + workerd: 1.20250718.0 + + '@cloudflare/workerd-darwin-64@1.20250718.0': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20250718.0': + optional: true + + '@cloudflare/workerd-linux-64@1.20250718.0': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20250718.0': + optional: true + + '@cloudflare/workerd-windows-64@1.20250718.0': + optional: true + + '@cloudflare/workers-types@4.20260702.1': {} + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.17.19)': + dependencies: + esbuild: 0.17.19 + + '@esbuild-plugins/node-modules-polyfill@0.2.2(esbuild@0.17.19)': + dependencies: + esbuild: 0.17.19 + escape-string-regexp: 4.0.0 + rollup-plugin-node-polyfills: 0.2.1 + + '@esbuild/android-arm64@0.17.19': + optional: true + + '@esbuild/android-arm@0.17.19': + optional: true + + '@esbuild/android-x64@0.17.19': + optional: true + + '@esbuild/darwin-arm64@0.17.19': + optional: true + + '@esbuild/darwin-x64@0.17.19': + optional: true + + '@esbuild/freebsd-arm64@0.17.19': + optional: true + + '@esbuild/freebsd-x64@0.17.19': + optional: true + + '@esbuild/linux-arm64@0.17.19': + optional: true + + '@esbuild/linux-arm@0.17.19': + optional: true + + '@esbuild/linux-ia32@0.17.19': + optional: true + + '@esbuild/linux-loong64@0.17.19': + optional: true + + '@esbuild/linux-mips64el@0.17.19': + optional: true + + '@esbuild/linux-ppc64@0.17.19': + optional: true + + '@esbuild/linux-riscv64@0.17.19': + optional: true + + '@esbuild/linux-s390x@0.17.19': + optional: true + + '@esbuild/linux-x64@0.17.19': + optional: true + + '@esbuild/netbsd-x64@0.17.19': + optional: true + + '@esbuild/openbsd-x64@0.17.19': + optional: true + + '@esbuild/sunos-x64@0.17.19': + optional: true + + '@esbuild/win32-arm64@0.17.19': + optional: true + + '@esbuild/win32-ia32@0.17.19': + optional: true + + '@esbuild/win32-x64@0.17.19': + optional: true + + '@fastify/busboy@2.1.1': {} + + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + acorn-walk@8.3.2: {} + + acorn@8.14.0: {} + + as-table@1.0.55: + dependencies: + printable-characters: 1.0.42 + + blake3-wasm@2.1.5: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + optional: true + + color-name@1.1.4: + optional: true + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.4 + optional: true + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + optional: true + + cookie@0.7.2: {} + + data-uri-to-buffer@2.0.2: {} + + defu@6.1.7: {} + + detect-libc@2.1.2: + optional: true + + esbuild@0.17.19: + optionalDependencies: + '@esbuild/android-arm': 0.17.19 + '@esbuild/android-arm64': 0.17.19 + '@esbuild/android-x64': 0.17.19 + '@esbuild/darwin-arm64': 0.17.19 + '@esbuild/darwin-x64': 0.17.19 + '@esbuild/freebsd-arm64': 0.17.19 + '@esbuild/freebsd-x64': 0.17.19 + '@esbuild/linux-arm': 0.17.19 + '@esbuild/linux-arm64': 0.17.19 + '@esbuild/linux-ia32': 0.17.19 + '@esbuild/linux-loong64': 0.17.19 + '@esbuild/linux-mips64el': 0.17.19 + '@esbuild/linux-ppc64': 0.17.19 + '@esbuild/linux-riscv64': 0.17.19 + '@esbuild/linux-s390x': 0.17.19 + '@esbuild/linux-x64': 0.17.19 + '@esbuild/netbsd-x64': 0.17.19 + '@esbuild/openbsd-x64': 0.17.19 + '@esbuild/sunos-x64': 0.17.19 + '@esbuild/win32-arm64': 0.17.19 + '@esbuild/win32-ia32': 0.17.19 + '@esbuild/win32-x64': 0.17.19 + + escape-string-regexp@4.0.0: {} + + estree-walker@0.6.1: {} + + exit-hook@2.2.1: {} + + exsolve@1.1.1: {} + + fsevents@2.3.3: + optional: true + + get-source@2.0.12: + dependencies: + data-uri-to-buffer: 2.0.2 + source-map: 0.6.1 + + glob-to-regexp@0.4.1: {} + + is-arrayish@0.3.4: + optional: true + + magic-string@0.25.9: + dependencies: + sourcemap-codec: 1.4.8 + + mime@3.0.0: {} + + miniflare@3.20250718.3: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + acorn: 8.14.0 + acorn-walk: 8.3.2 + exit-hook: 2.2.1 + glob-to-regexp: 0.4.1 + stoppable: 1.1.0 + undici: 5.29.0 + workerd: 1.20250718.0 + ws: 8.18.0 + youch: 3.3.4 + zod: 3.22.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + mustache@4.2.0: {} + + ohash@2.0.12: {} + + path-to-regexp@6.3.0: {} + + pathe@2.0.3: {} + + postal-mime@2.7.6: {} + + printable-characters@1.0.42: {} + + rollup-plugin-inject@3.0.2: + dependencies: + estree-walker: 0.6.1 + magic-string: 0.25.9 + rollup-pluginutils: 2.8.2 + + rollup-plugin-node-polyfills@0.2.1: + dependencies: + rollup-plugin-inject: 3.0.2 + + rollup-pluginutils@2.8.2: + dependencies: + estree-walker: 0.6.1 + + semver@7.8.5: + optional: true + + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + optional: true + + simple-swizzle@0.2.4: + dependencies: + is-arrayish: 0.3.4 + optional: true + + source-map@0.6.1: {} + + sourcemap-codec@1.4.8: {} + + stacktracey@2.2.0: + dependencies: + as-table: 1.0.55 + get-source: 2.0.12 + + stoppable@1.1.0: {} + + tslib@2.8.1: + optional: true + + typescript@6.0.3: {} + + ufo@1.6.4: {} + + undici@5.29.0: + dependencies: + '@fastify/busboy': 2.1.1 + + unenv@2.0.0-rc.14: + dependencies: + defu: 6.1.7 + exsolve: 1.1.1 + ohash: 2.0.12 + pathe: 2.0.3 + ufo: 1.6.4 + + workerd@1.20250718.0: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20250718.0 + '@cloudflare/workerd-darwin-arm64': 1.20250718.0 + '@cloudflare/workerd-linux-64': 1.20250718.0 + '@cloudflare/workerd-linux-arm64': 1.20250718.0 + '@cloudflare/workerd-windows-64': 1.20250718.0 + + wrangler@3.114.17(@cloudflare/workers-types@4.20260702.1): + dependencies: + '@cloudflare/kv-asset-handler': 0.3.4 + '@cloudflare/unenv-preset': 2.0.2(unenv@2.0.0-rc.14)(workerd@1.20250718.0) + '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) + '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) + blake3-wasm: 2.1.5 + esbuild: 0.17.19 + miniflare: 3.20250718.3 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.14 + workerd: 1.20250718.0 + optionalDependencies: + '@cloudflare/workers-types': 4.20260702.1 + fsevents: 2.3.3 + sharp: 0.33.5 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + ws@8.18.0: {} + + youch@3.3.4: + dependencies: + cookie: 0.7.2 + mustache: 4.2.0 + stacktracey: 2.2.0 + + zod@3.22.3: {} diff --git a/scripts/cloudflare-email-worker/src/index.ts b/scripts/cloudflare-email-worker/src/index.ts new file mode 100644 index 0000000..9814233 --- /dev/null +++ b/scripts/cloudflare-email-worker/src/index.ts @@ -0,0 +1,58 @@ +import PostalMime from 'postal-mime'; + +export interface Env { + DESK_WEBHOOK_URL: string; + DESK_WEBHOOK_SECRET: string; +} + +export default { + async email(message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext): Promise { + try { + const rawEmail = await new Response(message.raw).arrayBuffer(); + const parser = new PostalMime(); + const parsed = await parser.parse(rawEmail); + + const fromEmail = message.from || parsed.from?.address || ''; + const fromName = parsed.from?.name || ''; + const toEmail = message.to || (parsed.to && parsed.to[0]?.address) || ''; + const toName = (parsed.to && parsed.to[0]?.name) || ''; + const subject = message.headers.get('subject') || parsed.subject || ''; + const messageId = message.headers.get('message-id') || parsed.messageId || ''; + const inReplyTo = message.headers.get('in-reply-to') || parsed.inReplyTo || ''; + const references = message.headers.get('references') || (Array.isArray(parsed.references) ? parsed.references.join(' ') : parsed.references) || ''; + + const payload = { + from: fromEmail, + from_name: fromName, + to: toEmail, + to_name: toName, + subject: subject, + text: parsed.text || '', + html: parsed.html || '', + message_id: messageId, + in_reply_to: inReplyTo, + references: references, + }; + + const webhookUrl = env.DESK_WEBHOOK_URL || ''; + const webhookSecret = env.DESK_WEBHOOK_SECRET || ''; + + const response = await fetch(webhookUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Webhook-Secret': webhookSecret, + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + console.error(`Failed to forward email to webhook: ${response.status} ${response.statusText}`); + } else { + console.log(`Successfully forwarded email from ${fromEmail} to Agent Desk`); + } + } catch (error) { + console.error('Error processing inbound email in worker:', error); + } + }, +}; diff --git a/scripts/cloudflare-email-worker/wrangler.toml b/scripts/cloudflare-email-worker/wrangler.toml new file mode 100644 index 0000000..1c041e0 --- /dev/null +++ b/scripts/cloudflare-email-worker/wrangler.toml @@ -0,0 +1,8 @@ +name = "agent-desk-email-inbound-worker" +main = "src/index.ts" +compatibility_date = "2024-11-05" +account_id = "5f2a58925e790423dfafa0e6bee46b28" + +[vars] +DESK_WEBHOOK_URL = "https://your-desk.example.com/api/third/email/webhook" +DESK_WEBHOOK_SECRET = "replace-with-your-email-inbound-secret" diff --git a/scripts/i18n_untranslated.py b/scripts/i18n_untranslated.py new file mode 100644 index 0000000..cf3ba55 --- /dev/null +++ b/scripts/i18n_untranslated.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +"""Report i18n values that still hold the source language instead of a translation. + +Key parity is already enforced by the test suite +(web/i18n/message-integrity.test.mjs and +internal/pkg/i18nx/locale_parity_test.go), so this exists for the gap those +tests cannot see: a key can be present in every locale and still carry the +Chinese string, which is how Chinese kept showing up in an otherwise translated +UI. + +Usage: + python scripts/i18n_untranslated.py + + is the source language, the one being audited. Values in +containing CJK are listed. Those byte-identical to are reported as +untranslated; the rest are left to judgement, since product names and sample +data legitimately keep CJK. + +Exit code: 0 when the child has no untranslated values, 1 when it does, +2 on usage errors. +""" + +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path +from typing import Any + +# Hangul and Kana are deliberately excluded: this looks for Chinese script, +# which is what leaked into the UI, not for non-ASCII text in general. +CJK = re.compile(r"[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]") + +USAGE = "Usage: python scripts/i18n_untranslated.py \n" + + +def flatten_json(value: Any, prefix: str = "", out: dict[str, Any] | None = None) -> dict[str, Any]: + if out is None: + out = {} + if isinstance(value, dict): + for key, child in value.items(): + flatten_json(child, f"{prefix}.{key}" if prefix else str(key), out) + return out + out[prefix] = value + return out + + +def _indent_of(line: str) -> int: + return len(line) - len(line.lstrip()) + + +def parse_flat_yaml(text: str) -> dict[str, Any]: + """Parse the locale YAML dialect: flat top-level keys, one nested level, block scalars.""" + lines = text.splitlines() + out: dict[str, Any] = {} + index = 0 + + while index < len(lines): + raw = lines[index] + index += 1 + + if not raw.strip() or raw.lstrip().startswith("#"): + continue + + indent = _indent_of(raw) + if indent != 0: + continue + + head = raw.strip() + if ":" not in head: + continue + key, _, rest = head.partition(":") + key = key.strip().strip("\"'") + value = rest.strip() + + if value in ("|", ">", "|-", ">-", "|+", ">+"): + block: list[str] = [] + block_indent: int | None = None + while index < len(lines): + line = lines[index] + if line.strip() and _indent_of(line) <= indent: + break + line_indent = _indent_of(line) + block_indent = line_indent if block_indent is None else block_indent + block.append(line[min(block_indent, line_indent):]) + index += 1 + out[key] = "\n".join(block).strip() + elif not value: + # A key with no inline value opens one nested level. + while index < len(lines): + line = lines[index] + if line.strip() and _indent_of(line) <= indent: + break + stripped = line.strip() + index += 1 + if ":" in stripped: + nested_key, _, nested_value = stripped.partition(":") + out[f"{key}.{nested_key.strip()}"] = nested_value.strip().strip("\"'") + continue + else: + out[key] = value.strip().strip("\"'") + + return out + + +def load_values(file: str) -> dict[str, Any]: + suffix = Path(file).suffix.lower() + # utf-8-sig tolerates a BOM and a plain utf-8 file equally; a BOM is common + # in locale files authored on Windows and would otherwise abort the run. + text = Path(file).read_text(encoding="utf-8-sig") + if suffix == ".json": + return flatten_json(json.loads(text)) + if suffix in (".yml", ".yaml"): + return parse_flat_yaml(text) + raise ValueError(f"Unsupported file format: {file}") + + +def main(argv: list[str]) -> int: + args = [arg for arg in argv if not arg.startswith("-")] + if len(args) != 2: + sys.stderr.write(USAGE) + return 2 + + base_file, child_file = args + try: + base = load_values(base_file) + child = load_values(child_file) + except (OSError, ValueError, json.JSONDecodeError) as error: + sys.stderr.write(f"Failed to read bundle: {error}\n") + return 2 + + untranslated: list[tuple[str, str]] = [] + review: list[tuple[str, str]] = [] + for key, value in child.items(): + if not isinstance(value, str) or not CJK.search(value): + continue + base_value = base.get(key) + if isinstance(base_value, str) and base_value.strip() == value.strip(): + untranslated.append((key, value)) + else: + review.append((key, value)) + + print( + f"child {child_file}: {len(child)} values, " + f"{len(untranslated) + len(review)} with CJK " + f"({len(untranslated)} untranslated, {len(review)} to review)" + ) + + if untranslated: + print(f"\nuntranslated, identical to base ({len(untranslated)}):") + for key, value in untranslated: + print(f" - {key} = {value}") + + if review: + print(f"\nCJK but differs from base ({len(review)}):") + for key, value in review: + print(f" ? {key} = {value}") + + if not untranslated and not review: + print("OK: no CJK left in child values") + return 0 + return 1 + + +if __name__ == "__main__": + # Do not litter the repo with __pycache__ when this is run from tooling. + sys.dont_write_bytecode = True + raise SystemExit(main(sys.argv[1:])) \ No newline at end of file diff --git a/web/.env.example b/web/.env.example new file mode 100644 index 0000000..1464741 --- /dev/null +++ b/web/.env.example @@ -0,0 +1 @@ +NEXT_PUBLIC_API_BASE_URL=http://localhost:8083 \ No newline at end of file diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..7b8da95 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,42 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* +!.env.example + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..c6f6f47 --- /dev/null +++ b/web/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to self-host [Inter](https://rsms.me/inter/) for UI text and [IBM Plex Mono](https://www.ibm.com/plex/mono/) for monospace text, both declared once in `lib/fonts.ts` and wired into the design tokens through `--font-app-sans` / `--font-app-mono`. Latin and Cyrillic subsets are included so the `zh-CN`, `en-US`, and `ru-RU` locales all render from the same font files. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/web/app/(dashboard)/dashboard.css b/web/app/(dashboard)/dashboard.css new file mode 100644 index 0000000..f63b522 --- /dev/null +++ b/web/app/(dashboard)/dashboard.css @@ -0,0 +1,368 @@ +@import "tailwindcss"; +@import "tw-animate-css"; +@import "shadcn/tailwind.css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-app-sans), -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; + --font-mono: var(--font-app-mono), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); +} + +:root, +:root[data-palette="plain"] { + --radius: 0.5rem; + --background: #f9f9f9; + --foreground: #151923; + --card: #ffffff; + --card-foreground: #151923; + --popover: #ffffff; + --popover-foreground: #151923; + --primary: #2475fc; + --primary-foreground: #ffffff; + --secondary: #e6efff; + --secondary-foreground: #155be8; + --muted: #f4f7fc; + --muted-foreground: #667085; + --accent: #e6efff; + --accent-foreground: #155be8; + --destructive: oklch(0.59 0.225 25); + --border: #dfe7f3; + --input: #dfe7f3; + --ring: #2475fc; + --chart-1: #2475fc; + --chart-2: #14b8a6; + --chart-3: #f59e0b; + --chart-4: #ec4899; + --chart-5: #8b5cf6; + --sidebar: #ffffff; + --sidebar-foreground: #273142; + --sidebar-primary: #2475fc; + --sidebar-primary-foreground: #ffffff; + --sidebar-accent: #e6efff; + --sidebar-accent-foreground: #155be8; + --sidebar-border: #dfe7f3; + --sidebar-ring: #2475fc; +} + +.dark[data-palette="plain"] { + --background: #111827; + --foreground: #f9fafb; + --card: #182235; + --card-foreground: #f9fafb; + --popover: #182235; + --popover-foreground: #f9fafb; + --primary: #6da2ff; + --primary-foreground: #0f172a; + --secondary: #1f3354; + --secondary-foreground: #dbe8ff; + --muted: #1f2937; + --muted-foreground: #9ca3af; + --accent: #1f3354; + --accent-foreground: #dbe8ff; + --destructive: oklch(0.68 0.19 25); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 14%); + --ring: #6da2ff; + --chart-1: #6da2ff; + --chart-2: #2dd4bf; + --chart-3: #fbbf24; + --chart-4: #f472b6; + --chart-5: #a78bfa; + --sidebar: #111827; + --sidebar-foreground: #f3f4f6; + --sidebar-primary: #6da2ff; + --sidebar-primary-foreground: #0f172a; + --sidebar-accent: #1f3354; + --sidebar-accent-foreground: #dbe8ff; + --sidebar-border: oklch(1 0 0 / 9%); + --sidebar-ring: #6da2ff; +} + +:root[data-palette="green"] { + --background: oklch(0.973 0.011 112); + --foreground: oklch(0.22 0.021 210); + --card: oklch(0.996 0.004 105); + --card-foreground: oklch(0.22 0.021 210); + --popover: oklch(0.996 0.004 105); + --popover-foreground: oklch(0.22 0.021 210); + --primary: oklch(0.475 0.118 178); + --primary-foreground: oklch(0.985 0.012 170); + --secondary: oklch(0.94 0.018 124); + --secondary-foreground: oklch(0.255 0.035 178); + --muted: oklch(0.942 0.012 115); + --muted-foreground: oklch(0.49 0.026 205); + --accent: oklch(0.91 0.028 169); + --accent-foreground: oklch(0.245 0.045 178); + --destructive: oklch(0.59 0.225 25); + --border: oklch(0.872 0.018 116); + --input: oklch(0.872 0.018 116); + --ring: oklch(0.56 0.115 178); + --chart-1: oklch(0.52 0.13 178); + --chart-2: oklch(0.61 0.14 220); + --chart-3: oklch(0.72 0.15 78); + --chart-4: oklch(0.62 0.15 330); + --chart-5: oklch(0.58 0.12 262); + --sidebar: oklch(0.925 0.018 118); + --sidebar-foreground: oklch(0.245 0.03 190); + --sidebar-primary: oklch(0.475 0.118 178); + --sidebar-primary-foreground: oklch(0.985 0.012 170); + --sidebar-accent: oklch(0.89 0.032 156); + --sidebar-accent-foreground: oklch(0.235 0.048 178); + --sidebar-border: oklch(0.835 0.022 118); + --sidebar-ring: oklch(0.56 0.115 178); +} + +:root[data-palette="blue"] { + --background: oklch(0.982 0.007 250); + --foreground: oklch(0.205 0.018 255); + --card: oklch(0.998 0.002 250); + --card-foreground: oklch(0.205 0.018 255); + --popover: oklch(0.998 0.002 250); + --popover-foreground: oklch(0.205 0.018 255); + --primary: oklch(0.548 0.19 257); + --primary-foreground: oklch(0.985 0.006 248); + --secondary: oklch(0.948 0.013 250); + --secondary-foreground: oklch(0.245 0.02 252); + --muted: oklch(0.95 0.01 250); + --muted-foreground: oklch(0.49 0.024 245); + --accent: oklch(0.922 0.026 255); + --accent-foreground: oklch(0.255 0.044 254); + --destructive: oklch(0.59 0.225 25); + --border: oklch(0.888 0.014 250); + --input: oklch(0.888 0.014 250); + --ring: oklch(0.61 0.15 252); + --chart-1: oklch(0.59 0.18 255); + --chart-2: oklch(0.62 0.13 178); + --chart-3: oklch(0.72 0.16 82); + --chart-4: oklch(0.61 0.17 333); + --chart-5: oklch(0.56 0.13 285); + --sidebar: oklch(0.94 0.016 250); + --sidebar-foreground: oklch(0.245 0.024 252); + --sidebar-primary: oklch(0.548 0.19 257); + --sidebar-primary-foreground: oklch(0.985 0.006 248); + --sidebar-accent: oklch(0.908 0.025 252); + --sidebar-accent-foreground: oklch(0.245 0.038 254); + --sidebar-border: oklch(0.858 0.018 250); + --sidebar-ring: oklch(0.61 0.15 252); +} + +:root[data-palette="gray"] { + --background: oklch(0.974 0.006 170); + --foreground: oklch(0.205 0.018 255); + --card: oklch(0.998 0.001 106); + --card-foreground: oklch(0.205 0.018 255); + --popover: oklch(0.998 0.001 106); + --popover-foreground: oklch(0.205 0.018 255); + --primary: oklch(0.538 0.183 257); + --primary-foreground: oklch(0.985 0.006 248); + --secondary: oklch(0.944 0.011 175); + --secondary-foreground: oklch(0.245 0.018 250); + --muted: oklch(0.948 0.008 180); + --muted-foreground: oklch(0.49 0.024 245); + --accent: oklch(0.925 0.018 218); + --accent-foreground: oklch(0.255 0.033 250); + --destructive: oklch(0.59 0.225 25); + --border: oklch(0.886 0.012 178); + --input: oklch(0.886 0.012 178); + --ring: oklch(0.61 0.15 252); + --chart-1: oklch(0.59 0.18 255); + --chart-2: oklch(0.62 0.13 178); + --chart-3: oklch(0.72 0.16 82); + --chart-4: oklch(0.61 0.17 333); + --chart-5: oklch(0.56 0.13 285); + --sidebar: oklch(0.94 0.011 170); + --sidebar-foreground: oklch(0.255 0.022 250); + --sidebar-primary: oklch(0.538 0.183 257); + --sidebar-primary-foreground: oklch(0.985 0.006 248); + --sidebar-accent: oklch(0.904 0.017 190); + --sidebar-accent-foreground: oklch(0.245 0.028 250); + --sidebar-border: oklch(0.86 0.014 176); + --sidebar-ring: oklch(0.61 0.15 252); +} + +.dark { + --background: oklch(0.18 0 0); + --foreground: oklch(0.94 0 0); + --card: oklch(0.225 0 0); + --card-foreground: oklch(0.94 0 0); + --popover: oklch(0.225 0 0); + --popover-foreground: oklch(0.94 0 0); + --primary: oklch(0.82 0 0); + --primary-foreground: oklch(0.18 0 0); + --secondary: oklch(0.285 0 0); + --secondary-foreground: oklch(0.94 0 0); + --muted: oklch(0.285 0 0); + --muted-foreground: oklch(0.7 0 0); + --accent: oklch(0.32 0 0); + --accent-foreground: oklch(0.94 0 0); + --destructive: oklch(0.68 0.19 25); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 14%); + --ring: oklch(0.62 0 0); + --chart-1: oklch(0.82 0 0); + --chart-2: oklch(0.72 0 0); + --chart-3: oklch(0.62 0 0); + --chart-4: oklch(0.52 0 0); + --chart-5: oklch(0.42 0 0); + --sidebar: oklch(0.16 0 0); + --sidebar-foreground: oklch(0.93 0 0); + --sidebar-primary: oklch(0.82 0 0); + --sidebar-primary-foreground: oklch(0.18 0 0); + --sidebar-accent: oklch(0.27 0 0); + --sidebar-accent-foreground: oklch(0.94 0 0); + --sidebar-border: oklch(1 0 0 / 9%); + --sidebar-ring: oklch(0.62 0 0); +} + +.dark[data-palette="green"] { + --background: oklch(0.18 0.016 190); + --foreground: oklch(0.94 0.009 155); + --card: oklch(0.225 0.018 190); + --card-foreground: oklch(0.94 0.009 155); + --popover: oklch(0.225 0.018 190); + --popover-foreground: oklch(0.94 0.009 155); + --primary: oklch(0.67 0.13 174); + --primary-foreground: oklch(0.16 0.024 185); + --secondary: oklch(0.285 0.019 185); + --secondary-foreground: oklch(0.94 0.009 155); + --muted: oklch(0.285 0.019 185); + --muted-foreground: oklch(0.72 0.02 165); + --accent: oklch(0.33 0.032 174); + --accent-foreground: oklch(0.955 0.011 155); + --destructive: oklch(0.68 0.19 25); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 14%); + --ring: oklch(0.67 0.13 174); + --chart-1: oklch(0.68 0.13 174); + --chart-2: oklch(0.68 0.13 220); + --chart-3: oklch(0.75 0.15 78); + --chart-4: oklch(0.68 0.16 333); + --chart-5: oklch(0.67 0.13 262); + --sidebar: oklch(0.16 0.019 185); + --sidebar-foreground: oklch(0.93 0.01 155); + --sidebar-primary: oklch(0.67 0.13 174); + --sidebar-primary-foreground: oklch(0.16 0.024 185); + --sidebar-accent: oklch(0.27 0.028 178); + --sidebar-accent-foreground: oklch(0.955 0.011 155); + --sidebar-border: oklch(1 0 0 / 9%); + --sidebar-ring: oklch(0.67 0.13 174); +} + +.dark[data-palette="blue"] { + --background: oklch(0.178 0.012 245); + --foreground: oklch(0.948 0.006 230); + --card: oklch(0.226 0.014 245); + --card-foreground: oklch(0.948 0.006 230); + --popover: oklch(0.226 0.014 245); + --popover-foreground: oklch(0.948 0.006 230); + --primary: oklch(0.66 0.16 252); + --primary-foreground: oklch(0.168 0.015 250); + --secondary: oklch(0.285 0.014 245); + --secondary-foreground: oklch(0.94 0.006 230); + --muted: oklch(0.285 0.014 245); + --muted-foreground: oklch(0.72 0.015 238); + --accent: oklch(0.32 0.022 225); + --accent-foreground: oklch(0.955 0.006 230); + --destructive: oklch(0.68 0.19 25); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 14%); + --ring: oklch(0.66 0.16 252); + --chart-1: oklch(0.69 0.15 252); + --chart-2: oklch(0.68 0.12 178); + --chart-3: oklch(0.75 0.15 82); + --chart-4: oklch(0.68 0.16 333); + --chart-5: oklch(0.67 0.13 285); + --sidebar: oklch(0.158 0.012 248); + --sidebar-foreground: oklch(0.93 0.006 230); + --sidebar-primary: oklch(0.66 0.16 252); + --sidebar-primary-foreground: oklch(0.168 0.015 250); + --sidebar-accent: oklch(0.255 0.017 245); + --sidebar-accent-foreground: oklch(0.955 0.006 230); + --sidebar-border: oklch(1 0 0 / 9%); + --sidebar-ring: oklch(0.66 0.16 252); +} + +.dark[data-palette="gray"] { + --background: oklch(0.178 0.012 245); + --foreground: oklch(0.948 0.006 230); + --card: oklch(0.226 0.014 245); + --card-foreground: oklch(0.948 0.006 230); + --popover: oklch(0.226 0.014 245); + --popover-foreground: oklch(0.948 0.006 230); + --primary: oklch(0.66 0.16 252); + --primary-foreground: oklch(0.168 0.015 250); + --secondary: oklch(0.285 0.014 245); + --secondary-foreground: oklch(0.94 0.006 230); + --muted: oklch(0.285 0.014 245); + --muted-foreground: oklch(0.72 0.015 238); + --accent: oklch(0.32 0.022 225); + --accent-foreground: oklch(0.955 0.006 230); + --destructive: oklch(0.68 0.19 25); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 14%); + --ring: oklch(0.66 0.16 252); + --chart-1: oklch(0.69 0.15 252); + --chart-2: oklch(0.68 0.12 178); + --chart-3: oklch(0.75 0.15 82); + --chart-4: oklch(0.68 0.16 333); + --chart-5: oklch(0.67 0.13 285); + --sidebar: oklch(0.158 0.012 248); + --sidebar-foreground: oklch(0.93 0.006 230); + --sidebar-primary: oklch(0.66 0.16 252); + --sidebar-primary-foreground: oklch(0.168 0.015 250); + --sidebar-accent: oklch(0.255 0.017 245); + --sidebar-accent-foreground: oklch(0.955 0.006 230); + --sidebar-border: oklch(1 0 0 / 9%); + --sidebar-ring: oklch(0.66 0.16 252); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } + html { + @apply font-sans; + } +} diff --git a/web/app/(dashboard)/dashboard/_components/alert-list.tsx b/web/app/(dashboard)/dashboard/_components/alert-list.tsx new file mode 100644 index 0000000..fffd391 --- /dev/null +++ b/web/app/(dashboard)/dashboard/_components/alert-list.tsx @@ -0,0 +1,74 @@ +"use client" + +import Link from "next/link" +import { ArrowRightIcon, ShieldAlertIcon } from "lucide-react" + +import type { DashboardAlert } from "@/lib/api/dashboard" +import { Badge } from "@/components/ui/badge" +import { useI18n } from "@/i18n/provider" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" + +type AlertListProps = { + alerts: DashboardAlert[] +} + +function getAlertBadgeVariant(level: DashboardAlert["level"]) { + if (level === "error") { + return "destructive" as const + } + if (level === "warning") { + return "secondary" as const + } + return "outline" as const +} + +export function AlertList({ alerts }: AlertListProps) { + const t = useI18n() + + return ( + + + {t("dashboardHome.riskAlerts")} + {t("dashboardHome.riskAlertsDescription")} + + + {alerts.length === 0 ? ( +
+ +
{t("dashboardHome.noRiskTitle")}
+
+ {t("dashboardHome.noRiskDescription")} +
+
+ ) : ( + alerts.map((item) => ( + +
+
+
+
+
{item.title}
+ + {item.count} + +
+
+ {item.description} +
+
+ +
+
+ + )) + )} +
+
+ ) +} diff --git a/web/app/(dashboard)/dashboard/_components/dashboard-home.tsx b/web/app/(dashboard)/dashboard/_components/dashboard-home.tsx new file mode 100644 index 0000000..6118efa --- /dev/null +++ b/web/app/(dashboard)/dashboard/_components/dashboard-home.tsx @@ -0,0 +1,169 @@ +"use client" + +import { useCallback, useEffect, useState } from "react" +import { RefreshCwIcon } from "lucide-react" +import { toast } from "sonner" + +import { Button } from "@/components/ui/button" +import { Skeleton } from "@/components/ui/skeleton" +import { Card, CardContent } from "@/components/ui/card" +import { useI18n } from "@/i18n/provider" +import { + fetchDashboardOverview, + type DashboardOverview, + type DashboardRange, +} from "@/lib/api/dashboard" +import { SummaryCards } from "./summary-cards" +import { TrendPanel } from "./trend-panel" +import { TeamLoadPanel } from "./team-load-panel" +import { AlertList } from "./alert-list" +import { SetupReadinessBanner } from "./setup-readiness-banner" + +function LoadingCards() { + return ( +
+ {Array.from({ length: 6 }).map((_, index) => ( + + + + + + + + ))} +
+ ) +} + +export function DashboardHome() { + const t = useI18n() + const [range, setRange] = useState("7d") + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + const [refreshing, setRefreshing] = useState(false) + + const loadData = useCallback( + async (nextRange: DashboardRange, showRefreshing = false) => { + if (showRefreshing) { + setRefreshing(true) + } else { + setLoading(true) + } + try { + const result = await fetchDashboardOverview(nextRange) + setData(result) + } catch (error) { + toast.error(error instanceof Error ? error.message : t("dashboardHome.loadFailed")) + } finally { + setLoading(false) + setRefreshing(false) + } + }, + [t] + ) + + useEffect(() => { + void loadData(range) + }, [loadData, range]) + + const rangeOptions: Array<{ value: DashboardRange; label: string }> = [ + { value: "today", label: t("dashboardHome.rangeToday") }, + { value: "7d", label: t("dashboardHome.range7d") }, + { value: "30d", label: t("dashboardHome.range30d") }, + ] + + return ( +
+ void loadData(range, true)} /> +
+
+
+ {rangeOptions.map((item) => ( + + ))} +
+ +
+
+ + {loading && !data ? ( + + ) : data ? ( + <> + + + + +
+ + + + +
+
{t("dashboardHome.enabledAiAgents")}
+
{data.aiStats.enabledAiAgents}
+
+
+
{t("dashboardHome.enabledChannels")}
+
{data.aiStats.enabledChannels}
+
+
+
{t("dashboardHome.todayKnowledgeRetrieves")}
+
+ {data.aiStats.todayKnowledgeRetrieves} +
+
+
+
{t("dashboardHome.todayKnowledgeRetrieveFailRate")}
+
+ {data.aiStats.todayKnowledgeRetrieveFailRate.toFixed(1)}% +
+
+
+
{t("dashboardHome.todayAgentRunFailCount")}
+
+ {data.aiStats.todayAgentRunFailCount} +
+
+
+
{t("dashboardHome.todayAiHandoffCount")}
+
+ {data.aiStats.todayAiHandoffCount} +
+
+
+
+
+ + + + ) : ( + + + {t("dashboardHome.empty")} + + + )} +
+ ) +} diff --git a/web/app/(dashboard)/dashboard/_components/setup-readiness-banner.tsx b/web/app/(dashboard)/dashboard/_components/setup-readiness-banner.tsx new file mode 100644 index 0000000..0d485db --- /dev/null +++ b/web/app/(dashboard)/dashboard/_components/setup-readiness-banner.tsx @@ -0,0 +1,173 @@ +"use client" + +import { + AlertTriangleIcon, + ArrowRightIcon, + CheckCircle2Icon, + Loader2Icon, + SparklesIcon, +} from "lucide-react" +import Link from "next/link" +import { useCallback, useEffect, useState } from "react" +import { toast } from "sonner" + +import { + notifySetupReadinessUpdated, + SETUP_READINESS_UPDATED_EVENT, +} from "@/components/operator-readiness-indicator" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { useI18n } from "@/i18n/provider" +import { + applySetupQuickFix, + fetchSetupReadiness, + type SetupReadinessResponse, +} from "@/lib/api/setup-readiness" + +export function SetupReadinessBanner({ + onUpdated, +}: { + onUpdated?: () => void +}) { + const t = useI18n() + const [readiness, setReadiness] = useState(null) + const [fixing, setFixing] = useState(false) + + const loadReadiness = useCallback(async () => { + try { + const data = await fetchSetupReadiness() + setReadiness(data) + } catch { + // Ignore in banner if failed + } + }, []) + + useEffect(() => { + void loadReadiness() + const handleUpdated = () => { + void loadReadiness() + } + window.addEventListener(SETUP_READINESS_UPDATED_EVENT, handleUpdated) + return () => { + window.removeEventListener(SETUP_READINESS_UPDATED_EVENT, handleUpdated) + } + }, [loadReadiness]) + + async function handleQuickSetupAll() { + setFixing(true) + try { + const res = await applySetupQuickFix({ action: "quick_setup_all" }) + setReadiness(res.readiness) + notifySetupReadinessUpdated() + onUpdated?.() + toast.success(t("setupWizard.quickFixSuccess")) + } catch (error) { + toast.error( + error instanceof Error ? error.message : t("common.operationFailed"), + ) + } finally { + setFixing(false) + } + } + + if (!readiness) { + return null + } + + const allReady = + readiness.can_receive_requests_now && + readiness.current_operator.can_receive_now && + readiness.overall_status === "ok" + + if (allReady) { + return null + } + + const canQuickSetupAll = readiness.steps.some((s) => + s.quick_fix_actions.includes("quick_setup_all"), + ) + + const incompleteSteps = readiness.steps.filter((s) => s.status !== "ok") + + return ( +
+
+
+
+ +

+ {t("setupWizard.banner.title")} +

+ + {t("setupWizard.stepsProgress", { + ready: readiness.ready_steps, + total: readiness.total_steps, + })} + +
+

+ {t("setupWizard.banner.description")} +

+
+ {incompleteSteps.map((step) => ( + + {step.step_number}. {t(`setupWizard.steps.${step.key}.title`)} + + ))} + {!readiness.current_operator.can_receive_now + ? readiness.current_operator.blocker_codes.map((code) => ( + + {t(`setupWizard.operatorBlockers.${code}`)} + + )) + : null} +
+
+ +
+ {canQuickSetupAll ? ( + + ) : null} + +
+
+
+ ) +} + +export { CheckCircle2Icon } diff --git a/web/app/(dashboard)/dashboard/_components/summary-cards.tsx b/web/app/(dashboard)/dashboard/_components/summary-cards.tsx new file mode 100644 index 0000000..dd42612 --- /dev/null +++ b/web/app/(dashboard)/dashboard/_components/summary-cards.tsx @@ -0,0 +1,108 @@ +"use client" + +import Link from "next/link" +import { + BotMessageSquareIcon, + CircleDashedIcon, + HeadsetIcon, + SparklesIcon, + WavesIcon, +} from "lucide-react" + +import type { DashboardOverview } from "@/lib/api/dashboard" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { useI18n } from "@/i18n/provider" + +type SummaryCardsProps = { + summary: DashboardOverview["summary"] +} + +type SummaryCardItem = { + key: keyof DashboardOverview["summary"] + titleKey: string + descriptionKey: string + link: string + icon: typeof BotMessageSquareIcon + format?: (value: number) => string +} + +const cards: SummaryCardItem[] = [ + { + key: "todayNewConversations", + titleKey: "dashboardHome.summaryTodayNewConversations", + descriptionKey: "dashboardHome.summaryTodayNewConversationsDescription", + link: "/dashboard/conversations", + icon: BotMessageSquareIcon, + }, + { + key: "processingConversations", + titleKey: "dashboardHome.summaryProcessingConversations", + descriptionKey: "dashboardHome.summaryProcessingConversationsDescription", + link: "/dashboard/conversations", + icon: WavesIcon, + }, + { + key: "pendingDispatchConversations", + titleKey: "dashboardHome.summaryPendingDispatchConversations", + descriptionKey: "dashboardHome.summaryPendingDispatchConversationsDescription", + link: "/dashboard/conversations", + icon: CircleDashedIcon, + }, + { + key: "onlineAgents", + titleKey: "dashboardHome.summaryOnlineAgents", + descriptionKey: "dashboardHome.summaryOnlineAgentsDescription", + link: "/dashboard/agents", + icon: HeadsetIcon, + }, + { + key: "aiServiceRate", + titleKey: "dashboardHome.summaryAiServiceRate", + descriptionKey: "dashboardHome.summaryAiServiceRateDescription", + link: "/dashboard/ai-agents", + icon: SparklesIcon, + format: (value: number) => `${value.toFixed(1)}%`, + }, +] + +export function SummaryCards({ summary }: SummaryCardsProps) { + const t = useI18n() + + return ( +
+ {cards.map((item) => { + const Icon = item.icon + const rawValue = summary[item.key] + const value = + typeof item.format === "function" + ? item.format(Number(rawValue)) + : Number(rawValue).toLocaleString() + + return ( + + + +
+ {t(item.titleKey)} + {t(item.descriptionKey)} +
+
+ +
+
+ +
{value}
+
+
+ + ) + })} +
+ ) +} diff --git a/web/app/(dashboard)/dashboard/_components/team-load-panel.tsx b/web/app/(dashboard)/dashboard/_components/team-load-panel.tsx new file mode 100644 index 0000000..ad2b8bf --- /dev/null +++ b/web/app/(dashboard)/dashboard/_components/team-load-panel.tsx @@ -0,0 +1,112 @@ +"use client" + +import { Badge } from "@/components/ui/badge" +import { useI18n } from "@/i18n/provider" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import type { DashboardOverview } from "@/lib/api/dashboard" + +type TeamLoadPanelProps = { + agentStats: DashboardOverview["agentStats"] +} + +function getLoadTone(loadRate: number) { + if (loadRate >= 85) { + return "bg-red-500" + } + if (loadRate >= 60) { + return "bg-amber-500" + } + return "bg-emerald-500" +} + +export function TeamLoadPanel({ agentStats }: TeamLoadPanelProps) { + const t = useI18n() + + return ( + + + {t("dashboardHome.teamLoad")} + + {t("dashboardHome.teamLoadDescription", { + online: agentStats.onlineAgents, + busy: agentStats.busyAgents, + offline: agentStats.offlineAgents, + })} + + + + {agentStats.teamLoads.length === 0 ? ( +
+ {t("dashboardHome.noTeamData")} +
+ ) : ( + agentStats.teamLoads.map((item) => ( +
+
+
+
+
{item.teamName}
+ {item.hasScheduleNow ? ( + {t("dashboardHome.scheduled")} + ) : ( + {t("dashboardHome.notScheduled")} + )} +
+
+ {t("dashboardHome.teamStats", { + total: item.totalAgents, + online: item.onlineAgents, + busy: item.busyAgents, + offline: item.offlineAgents, + })} +
+
+
+
{item.loadRate.toFixed(1)}%
+
+ {t("dashboardHome.load", { + processing: item.processingConversations, + capacity: item.maxConcurrentCapacity || 0, + })} +
+
+
+ +
+
+
+ +
+
+
{t("dashboardHome.waiting")}
+
{item.waitingConversations}
+
+
+
{t("dashboardHome.processing")}
+
{item.processingConversations}
+
+
+
{t("dashboardHome.capacity")}
+
{item.maxConcurrentCapacity}
+
+
+
{t("dashboardHome.busyAgents")}
+
{item.busyAgents}
+
+
+
+ )) + )} + + + ) +} diff --git a/web/app/(dashboard)/dashboard/_components/trend-panel.tsx b/web/app/(dashboard)/dashboard/_components/trend-panel.tsx new file mode 100644 index 0000000..f18adfa --- /dev/null +++ b/web/app/(dashboard)/dashboard/_components/trend-panel.tsx @@ -0,0 +1,141 @@ +"use client" + +import { Area, AreaChart, Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts" + +import type { DashboardStatusDistributionItem, DashboardTrendItem } from "@/lib/api/dashboard" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { + ChartContainer, + ChartTooltip, + ChartTooltipContent, + type ChartConfig, +} from "@/components/ui/chart" +import { useI18n } from "@/i18n/provider" + +type TrendPanelProps = { + title: string + description: string + trend: DashboardTrendItem[] + distribution: DashboardStatusDistributionItem[] +} + +export function TrendPanel({ + title, + description, + trend, + distribution, +}: TrendPanelProps) { + const t = useI18n() + const trendConfig = { + newCount: { + label: t("dashboardHome.chartNew"), + color: "hsl(24 95% 53%)", + }, + closedCount: { + label: t("dashboardHome.chartClosed"), + color: "hsl(190 95% 39%)", + }, + } satisfies ChartConfig + const distributionConfig = { + count: { + label: t("dashboardHome.chartCount"), + theme: { + light: "hsl(222 47% 11%)", + dark: "hsl(210 40% 98%)", + }, + }, + } satisfies ChartConfig + const localizedDistribution = distribution.map((item) => ({ + ...item, + label: getStatusLabel(item.status, item.label, t), + })) + + return ( +
+ + + {title} + {description} + + + + + + + } /> + + + + + + + + + + {t("dashboardHome.statusDistribution")} + {t("dashboardHome.statusDistributionDescription")} + + + + + + + + } /> + + + + + +
+ ) +} + +function getStatusLabel( + status: number, + fallback: string, + t: (key: string) => string +) { + switch (status) { + case 1: + return t("dashboardHome.statusAiServing") + case 2: + return t("dashboardHome.statusPending") + case 3: + return t("dashboardHome.statusActive") + case 4: + return t("dashboardHome.statusClosed") + default: + return fallback + } +} diff --git a/web/app/(dashboard)/dashboard/agent-runs/page.tsx b/web/app/(dashboard)/dashboard/agent-runs/page.tsx new file mode 100644 index 0000000..e54e6a7 --- /dev/null +++ b/web/app/(dashboard)/dashboard/agent-runs/page.tsx @@ -0,0 +1,163 @@ +"use client" + +import { useEffect, useState } from "react" +import { AlertTriangleIcon, BotMessageSquareIcon, Clock3Icon, WorkflowIcon, WrenchIcon } from "lucide-react" +import { toast } from "sonner" + +import { DashboardListPage } from "@/components/dashboard/list" +import { JsonTreeViewer } from "@/components/json-tree-viewer" +import { OptionCombobox } from "@/components/option-combobox" +import { ProjectDialog } from "@/components/project-dialog" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Textarea } from "@/components/ui/textarea" +import { fetchAgentRun, fetchAgentRunMetrics, fetchAgentRuns, fetchAIWorkflowRun, saveAgentRunQualityFeedback, type AgentRun, type AgentRunMetrics, type AgentStep, type AgentToolCall, type AIWorkflowRun } from "@/lib/api/admin" +import { formatDateTime } from "@/lib/utils" +import { useI18n } from "@/i18n/provider" +import { WorkflowRunAuditGraph } from "../ai-workflow-runs/_components/workflow-run-audit-graph" + +function statusVariant(status: string) { + if (status === "failed") return "destructive" as const + if (status === "interrupted") return "outline" as const + if (status === "completed") return "default" as const + return "secondary" as const +} + +export default function DashboardAgentRunsPage() { + const t = useI18n() + const [open, setOpen] = useState(false) + const [loading, setLoading] = useState(false) + const [run, setRun] = useState(null) + const [workflowAuditOpen, setWorkflowAuditOpen] = useState(false) + const [workflowAuditLoading, setWorkflowAuditLoading] = useState(false) + const [workflowRun, setWorkflowRun] = useState(null) + const [metrics, setMetrics] = useState(null) + + useEffect(() => { + void fetchAgentRunMetrics().then(setMetrics).catch(() => setMetrics(null)) + }, []) + + async function openDetail(id: number) { + setOpen(true) + setLoading(true) + try { + setRun(await fetchAgentRun(id)) + } catch (error) { + toast.error(error instanceof Error ? error.message : t("agentRun.loadDetailFailed")) + setOpen(false) + } finally { + setLoading(false) + } + } + + async function openWorkflowAudit(id: number) { + if (id <= 0) return + setWorkflowAuditOpen(true) + setWorkflowAuditLoading(true) + try { + setWorkflowRun(await fetchAIWorkflowRun(id)) + } catch (error) { + toast.error(error instanceof Error ? error.message : t("aiWorkflow.loadDetailFailed")) + setWorkflowAuditOpen(false) + } finally { + setWorkflowAuditLoading(false) + } + } + + return ( + <> + {metrics ?
+ + + + + + + + + + +
: null} + + filters={[ + { name: "conversationId", label: t("agentRun.conversation"), defaultValue: "", valueType: "number", className: "w-full sm:w-40" }, + { name: "aiAgentId", label: t("agentRun.agent"), defaultValue: "", valueType: "number", className: "w-full sm:w-40" }, + { name: "status", label: t("agentRun.status"), defaultValue: "", className: "w-full sm:w-40" }, + ]} + fetchList={fetchAgentRuns} + getItemId={(item) => item.id} + getRowClassName={() => "cursor-pointer"} + onRowClick={(item) => void openDetail(item.id)} + columns={[ + { key: "startedAt", label: t("agentRun.startedAt"), className: "w-42 text-xs text-muted-foreground", render: (item) => formatDateTime(item.startedAt || item.createdAt) }, + { key: "agent", label: t("agentRun.agent"), className: "w-28", render: (item) => `#${item.aiAgentId || "-"}` }, + { key: "conversation", label: t("agentRun.conversation"), className: "w-28", render: (item) => `#${item.conversationId || "-"}` }, + { key: "status", label: t("agentRun.status"), className: "w-30", render: (item) => {item.status || "-"} }, + { key: "duration", label: t("agentRun.duration"), className: "w-24 text-right", render: (item) => `${item.durationMs || 0} ms` }, + { key: "tokens", label: t("agentRun.tokens"), className: "w-28 text-right", render: (item) => `${item.promptTokens || 0}/${item.completionTokens || 0}` }, + { key: "error", label: t("agentRun.error"), className: "w-72 max-w-72", render: (item) => item.errorMessage ? {item.errorMessage} : "-" }, + ]} + labels={{ refresh: t("agentRun.refresh"), query: t("agentRun.query"), loading: t("agentRun.loading"), empty: t("agentRun.empty"), loadFailed: t("agentRun.loadFailed") }} + /> + void openDetail(id)} onOpenChange={(next) => { setOpen(next); if (!next) setRun(null) }} t={t} /> + { setWorkflowAuditOpen(next); if (!next) setWorkflowRun(null) }} /> + + ) +} + +function Metric({ label, value, detail }: { label: string; value: string; detail: string }) { return
{label}
{value}
{detail}
} + +function AgentRunDetailDialog({ open, loading, run, onOpenChange, onOpenWorkflowAudit, onQualityFeedbackSaved, t }: { open: boolean; loading: boolean; run: AgentRun | null; onOpenChange: (open: boolean) => void; onOpenWorkflowAudit: (workflowRunId: number) => void; onQualityFeedbackSaved: (agentRunId: number) => void; t: (key: string) => string }) { + return {t("agentRun.detailTitle")}} description={run ? `Run #${run.id}` : t("agentRun.detailDescription")} footer={}> + {loading ?
{t("agentRun.loadingDetail")}
: run ?
+
+ {run.workflowRunId > 0 ?
{t("agentRun.workflowAudit")}
Workflow Run #{run.workflowRunId}
: null} + + {run.errorMessage ?
{run.errorMessage}
: null} + +

{t("agentRun.steps")}

{(run.steps ?? []).map((step) => )}{!run.steps?.length ?

{t("agentRun.emptySteps")}

: null}
+

{t("agentRun.toolCalls")}

{(run.toolCalls ?? []).map((call) => )}{!run.toolCalls?.length ?

{t("agentRun.emptyToolCalls")}

: null}
+
:
{t("agentRun.notFound")}
} +
+} + +function QualityFeedbackPanel({ run, onSaved, t }: { run: AgentRun; onSaved: (agentRunId: number) => void; t: (key: string) => string }) { + const [resolutionStatus, setResolutionStatus] = useState<"unknown" | "resolved" | "unresolved">("unknown") + const [evidenceStatus, setEvidenceStatus] = useState<"unknown" | "supported" | "unsupported">("unknown") + const [comment, setComment] = useState("") + const [saving, setSaving] = useState(false) + + useEffect(() => { + setResolutionStatus(run.qualityFeedback?.resolutionStatus ?? "unknown") + setEvidenceStatus(run.qualityFeedback?.evidenceStatus ?? "unknown") + setComment(run.qualityFeedback?.comment ?? "") + }, [run.id, run.qualityFeedback]) + + async function save() { + setSaving(true) + try { + await saveAgentRunQualityFeedback({ agentRunId: run.id, resolutionStatus, evidenceStatus, comment }) + toast.success(t("agentRun.qualitySaved")) + onSaved(run.id) + } catch (error) { + toast.error(error instanceof Error ? error.message : "Save failed") + } finally { + setSaving(false) + } + } + + return

{t("agentRun.qualityTitle")}

{t("agentRun.qualityDescription")}

setResolutionStatus(value === "resolved" || value === "unresolved" ? value : "unknown")} /> setEvidenceStatus(value === "supported" || value === "unsupported" ? value : "unknown")} />