Compare commits

..

16 Commits

Author SHA1 Message Date
moonrailgun
162954606a feat: add auto language detect for browser 2024-11-11 01:33:06 +08:00
moonrailgun
3bf86b3e6e feat: add audit log clear feature 2024-11-10 06:08:22 +08:00
moonrailgun
843a581d42 feat: add subscription selection page 2024-11-09 20:28:27 +08:00
moonrailgun
fffc989336 chore: remove unused script 2024-11-09 20:27:35 +08:00
moonrailgun
ea75ed7f88 chore: add alert 2024-11-09 20:27:17 +08:00
moonrailgun
34f9fe6957 refactor: add usage limit and update card style 2024-11-08 01:57:35 +08:00
moonrailgun
71f75c27dd feat: add api key and usage to command panel 2024-11-08 01:56:07 +08:00
moonrailgun
a12fa3e6fe feat: add <UsageCard /> component which can render usage data and progress 2024-11-08 01:47:49 +08:00
moonrailgun
ae5f5a97d9 chore: remove passport package 2024-11-08 00:28:04 +08:00
moonrailgun
31ad64cd95 feat: add cronjob to check workspace limit which will pause workspace 2024-11-07 00:06:04 +08:00
tommy141x
1096e9ca9a Fix number casting issue 2024-11-06 12:11:08 +08:00
moonrailgun
b71bf6542e feat: add more usage stats 2024-11-06 01:19:57 +08:00
moonrailgun
e4b98b1c36 feat: add workspace subscription 2024-11-06 01:11:03 +08:00
moonrailgun
fa1ff3b5f6 refactor: move billing mode inside folder 2024-11-06 01:11:03 +08:00
moonrailgun
f0ddf6c5dd refactor: add apikey check before setup 2024-11-06 01:11:03 +08:00
moonrailgun
74d391afc1 feat: add lemonsqueezy subscription 2024-11-06 01:11:03 +08:00
51 changed files with 983 additions and 271 deletions

View File

@ -135,8 +135,8 @@ importers:
specifier: ^3.3.4 specifier: ^3.3.4
version: 3.3.4(react-hook-form@7.51.1(react@18.2.0)) version: 3.3.4(react-hook-form@7.51.1(react@18.2.0))
'@i18next-toolkit/react': '@i18next-toolkit/react':
specifier: ^1.1.0 specifier: 2.0.0-rc.5
version: 1.1.0(@types/react@18.2.78)(buffer@6.0.3)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) version: 2.0.0-rc.5(@types/react@18.2.78)(buffer@6.0.3)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@loadable/component': '@loadable/component':
specifier: ^5.16.3 specifier: ^5.16.3
version: 5.16.3(react@18.2.0) version: 5.16.3(react@18.2.0)
@ -543,12 +543,9 @@ importers:
nodemailer: nodemailer:
specifier: ^6.9.8 specifier: ^6.9.8
version: 6.9.8 version: 6.9.8
passport: p-map:
specifier: ^0.7.0 specifier: 4.0.0
version: 0.7.0 version: 4.0.0
passport-jwt:
specifier: ^4.0.1
version: 4.0.1
ping: ping:
specifier: ^0.4.4 specifier: ^0.4.4
version: 0.4.4 version: 0.4.4
@ -634,12 +631,6 @@ importers:
'@types/nodemailer': '@types/nodemailer':
specifier: ^6.4.11 specifier: ^6.4.11
version: 6.4.11 version: 6.4.11
'@types/passport':
specifier: ^1.0.12
version: 1.0.12
'@types/passport-jwt':
specifier: ^3.0.9
version: 3.0.9
'@types/ping': '@types/ping':
specifier: ^0.4.2 specifier: ^0.4.2
version: 0.4.2 version: 0.4.2
@ -661,9 +652,6 @@ importers:
execa: execa:
specifier: ^5.1.1 specifier: ^5.1.1
version: 5.1.1 version: 5.1.1
p-map:
specifier: 4.0.0
version: 4.0.0
prisma: prisma:
specifier: 5.14.0 specifier: 5.14.0
version: 5.14.0 version: 5.14.0
@ -2263,8 +2251,14 @@ packages:
'@i18next-toolkit/extractor@1.1.0': '@i18next-toolkit/extractor@1.1.0':
resolution: {integrity: sha512-USq83a1XKKCRGqlaKBoNRuCImD1IDFCHMgDHs9686v3IpZ2wQdj/e11+cPaGX1UIjndZKULdQq4b0aZJyMrBfg==} resolution: {integrity: sha512-USq83a1XKKCRGqlaKBoNRuCImD1IDFCHMgDHs9686v3IpZ2wQdj/e11+cPaGX1UIjndZKULdQq4b0aZJyMrBfg==}
'@i18next-toolkit/react@1.1.0': '@i18next-toolkit/react-core@1.1.0':
resolution: {integrity: sha512-S9HFkBwCukCwRR18P4yhskzoBJwIJ2W182GQ9u5H69Guj3Sg4Lm0ghGk7VVALS4z3XmQNcJBzA+LWmLB2X5hIQ==} resolution: {integrity: sha512-PkuBaIY8jLS0QKy1sjj0g0XAC7zLIDM8ckI5VZGY3feiCPjV6ZdFsuC3cJJwRH+LgOCTj49LurhBii5UXGBlVQ==}
peerDependencies:
'@types/react': ^18.2.55
react: ^18.2.0
'@i18next-toolkit/react@2.0.0-rc.5':
resolution: {integrity: sha512-ZiQaLwS3jnYgFrotDTPbcF74Wbs0JNw0DDouuY+qNegUt5QxLBf/sCymQsglt9C9u2KOU+2CjS3ZP5NWlRknOg==}
peerDependencies: peerDependencies:
'@types/react': ^18.2.55 '@types/react': ^18.2.55
react: ^18.2.0 react: ^18.2.0
@ -2341,10 +2335,6 @@ packages:
resolution: {integrity: sha512-gM/FdNsK3BlrD6JRrhmiyqBXQsCpzSUdKSoZwJMQfXqfqcK321og+uMssc6HYcygUMrGvPnNJyJ1RqZPFDrgtg==} resolution: {integrity: sha512-gM/FdNsK3BlrD6JRrhmiyqBXQsCpzSUdKSoZwJMQfXqfqcK321og+uMssc6HYcygUMrGvPnNJyJ1RqZPFDrgtg==}
engines: {node: '>=20'} engines: {node: '>=20'}
'@ljharb/resumer@0.0.1':
resolution: {integrity: sha512-skQiAOrCfO7vRTq53cxznMpks7wS1va95UCidALlOVWqvBAzwPVErwizDwoMqNVMEn1mDq0utxZd02eIrvF1lw==}
engines: {node: '>= 0.4'}
'@ljharb/through@2.3.11': '@ljharb/through@2.3.11':
resolution: {integrity: sha512-ccfcIDlogiXNq5KcbAwbaO7lMh3Tm1i3khMPYpxlK8hH/W53zN81KM9coerRLOnTGu3nfXIniAmQbRI9OxbC0w==} resolution: {integrity: sha512-ccfcIDlogiXNq5KcbAwbaO7lMh3Tm1i3khMPYpxlK8hH/W53zN81KM9coerRLOnTGu3nfXIniAmQbRI9OxbC0w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -2449,24 +2439,28 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@next/swc-linux-arm64-musl@14.1.3': '@next/swc-linux-arm64-musl@14.1.3':
resolution: {integrity: sha512-esk1RkRBLSIEp1qaQXv1+s6ZdYzuVCnDAZySpa62iFTMGTisCyNQmqyCTL9P+cLJ4N9FKCI3ojtSfsyPHJDQNw==} resolution: {integrity: sha512-esk1RkRBLSIEp1qaQXv1+s6ZdYzuVCnDAZySpa62iFTMGTisCyNQmqyCTL9P+cLJ4N9FKCI3ojtSfsyPHJDQNw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@next/swc-linux-x64-gnu@14.1.3': '@next/swc-linux-x64-gnu@14.1.3':
resolution: {integrity: sha512-8uOgRlYEYiKo0L8YGeS+3TudHVDWDjPVDUcST+z+dUzgBbTEwSSIaSgF/vkcC1T/iwl4QX9iuUyUdQEl0Kxalg==} resolution: {integrity: sha512-8uOgRlYEYiKo0L8YGeS+3TudHVDWDjPVDUcST+z+dUzgBbTEwSSIaSgF/vkcC1T/iwl4QX9iuUyUdQEl0Kxalg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@next/swc-linux-x64-musl@14.1.3': '@next/swc-linux-x64-musl@14.1.3':
resolution: {integrity: sha512-DX2zqz05ziElLoxskgHasaJBREC5Y9TJcbR2LYqu4r7naff25B4iXkfXWfcp69uD75/0URmmoSgT8JclJtrBoQ==} resolution: {integrity: sha512-DX2zqz05ziElLoxskgHasaJBREC5Y9TJcbR2LYqu4r7naff25B4iXkfXWfcp69uD75/0URmmoSgT8JclJtrBoQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@next/swc-win32-arm64-msvc@14.1.3': '@next/swc-win32-arm64-msvc@14.1.3':
resolution: {integrity: sha512-HjssFsCdsD4GHstXSQxsi2l70F/5FsRTRQp8xNgmQs15SxUfUJRvSI9qKny/jLkY3gLgiCR3+6A7wzzK0DBlfA==} resolution: {integrity: sha512-HjssFsCdsD4GHstXSQxsi2l70F/5FsRTRQp8xNgmQs15SxUfUJRvSI9qKny/jLkY3gLgiCR3+6A7wzzK0DBlfA==}
@ -3351,6 +3345,7 @@ packages:
resolution: {integrity: sha512-MXg1xp+e5GhZ3Vit1gGEyoC+dyQUBy2JgVQ+3hUrD9wZMkUw/ywgkpK7oZgnB6kPpGrxJ41clkPPnsknuD6M2Q==} resolution: {integrity: sha512-MXg1xp+e5GhZ3Vit1gGEyoC+dyQUBy2JgVQ+3hUrD9wZMkUw/ywgkpK7oZgnB6kPpGrxJ41clkPPnsknuD6M2Q==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-gnueabihf@4.9.5': '@rollup/rollup-linux-arm-gnueabihf@4.9.5':
resolution: {integrity: sha512-Q0LcU61v92tQB6ae+udZvOyZ0wfpGojtAKrrpAaIqmJ7+psq4cMIhT/9lfV6UQIpeItnq/2QDROhNLo00lOD1g==} resolution: {integrity: sha512-Q0LcU61v92tQB6ae+udZvOyZ0wfpGojtAKrrpAaIqmJ7+psq4cMIhT/9lfV6UQIpeItnq/2QDROhNLo00lOD1g==}
@ -3361,66 +3356,79 @@ packages:
resolution: {integrity: sha512-DZNLwIY4ftPSRVkJEaxYkq7u2zel7aah57HESuNkUnz+3bZHxwkCUkrfS2IWC1sxK6F2QNIR0Qr/YXw7nkF3Pw==} resolution: {integrity: sha512-DZNLwIY4ftPSRVkJEaxYkq7u2zel7aah57HESuNkUnz+3bZHxwkCUkrfS2IWC1sxK6F2QNIR0Qr/YXw7nkF3Pw==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.19.1': '@rollup/rollup-linux-arm64-gnu@4.19.1':
resolution: {integrity: sha512-C7evongnjyxdngSDRRSQv5GvyfISizgtk9RM+z2biV5kY6S/NF/wta7K+DanmktC5DkuaJQgoKGf7KUDmA7RUw==} resolution: {integrity: sha512-C7evongnjyxdngSDRRSQv5GvyfISizgtk9RM+z2biV5kY6S/NF/wta7K+DanmktC5DkuaJQgoKGf7KUDmA7RUw==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-gnu@4.9.5': '@rollup/rollup-linux-arm64-gnu@4.9.5':
resolution: {integrity: sha512-dkRscpM+RrR2Ee3eOQmRWFjmV/payHEOrjyq1VZegRUa5OrZJ2MAxBNs05bZuY0YCtpqETDy1Ix4i/hRqX98cA==} resolution: {integrity: sha512-dkRscpM+RrR2Ee3eOQmRWFjmV/payHEOrjyq1VZegRUa5OrZJ2MAxBNs05bZuY0YCtpqETDy1Ix4i/hRqX98cA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.19.1': '@rollup/rollup-linux-arm64-musl@4.19.1':
resolution: {integrity: sha512-89tFWqxfxLLHkAthAcrTs9etAoBFRduNfWdl2xUs/yLV+7XDrJ5yuXMHptNqf1Zw0UCA3cAutkAiAokYCkaPtw==} resolution: {integrity: sha512-89tFWqxfxLLHkAthAcrTs9etAoBFRduNfWdl2xUs/yLV+7XDrJ5yuXMHptNqf1Zw0UCA3cAutkAiAokYCkaPtw==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-musl@4.9.5': '@rollup/rollup-linux-arm64-musl@4.9.5':
resolution: {integrity: sha512-QaKFVOzzST2xzY4MAmiDmURagWLFh+zZtttuEnuNn19AiZ0T3fhPyjPPGwLNdiDT82ZE91hnfJsUiDwF9DClIQ==} resolution: {integrity: sha512-QaKFVOzzST2xzY4MAmiDmURagWLFh+zZtttuEnuNn19AiZ0T3fhPyjPPGwLNdiDT82ZE91hnfJsUiDwF9DClIQ==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-powerpc64le-gnu@4.19.1': '@rollup/rollup-linux-powerpc64le-gnu@4.19.1':
resolution: {integrity: sha512-PromGeV50sq+YfaisG8W3fd+Cl6mnOOiNv2qKKqKCpiiEke2KiKVyDqG/Mb9GWKbYMHj5a01fq/qlUR28PFhCQ==} resolution: {integrity: sha512-PromGeV50sq+YfaisG8W3fd+Cl6mnOOiNv2qKKqKCpiiEke2KiKVyDqG/Mb9GWKbYMHj5a01fq/qlUR28PFhCQ==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.19.1': '@rollup/rollup-linux-riscv64-gnu@4.19.1':
resolution: {integrity: sha512-/1BmHYh+iz0cNCP0oHCuF8CSiNj0JOGf0jRlSo3L/FAyZyG2rGBuKpkZVH9YF+x58r1jgWxvm1aRg3DHrLDt6A==} resolution: {integrity: sha512-/1BmHYh+iz0cNCP0oHCuF8CSiNj0JOGf0jRlSo3L/FAyZyG2rGBuKpkZVH9YF+x58r1jgWxvm1aRg3DHrLDt6A==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.9.5': '@rollup/rollup-linux-riscv64-gnu@4.9.5':
resolution: {integrity: sha512-HeGqmRJuyVg6/X6MpE2ur7GbymBPS8Np0S/vQFHDmocfORT+Zt76qu+69NUoxXzGqVP1pzaY6QIi0FJWLC3OPA==} resolution: {integrity: sha512-HeGqmRJuyVg6/X6MpE2ur7GbymBPS8Np0S/vQFHDmocfORT+Zt76qu+69NUoxXzGqVP1pzaY6QIi0FJWLC3OPA==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-s390x-gnu@4.19.1': '@rollup/rollup-linux-s390x-gnu@4.19.1':
resolution: {integrity: sha512-0cYP5rGkQWRZKy9/HtsWVStLXzCF3cCBTRI+qRL8Z+wkYlqN7zrSYm6FuY5Kd5ysS5aH0q5lVgb/WbG4jqXN1Q==} resolution: {integrity: sha512-0cYP5rGkQWRZKy9/HtsWVStLXzCF3cCBTRI+qRL8Z+wkYlqN7zrSYm6FuY5Kd5ysS5aH0q5lVgb/WbG4jqXN1Q==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.19.1': '@rollup/rollup-linux-x64-gnu@4.19.1':
resolution: {integrity: sha512-XUXeI9eM8rMP8aGvii/aOOiMvTs7xlCosq9xCjcqI9+5hBxtjDpD+7Abm1ZhVIFE1J2h2VIg0t2DX/gjespC2Q==} resolution: {integrity: sha512-XUXeI9eM8rMP8aGvii/aOOiMvTs7xlCosq9xCjcqI9+5hBxtjDpD+7Abm1ZhVIFE1J2h2VIg0t2DX/gjespC2Q==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.9.5': '@rollup/rollup-linux-x64-gnu@4.9.5':
resolution: {integrity: sha512-Dq1bqBdLaZ1Gb/l2e5/+o3B18+8TI9ANlA1SkejZqDgdU/jK/ThYaMPMJpVMMXy2uRHvGKbkz9vheVGdq3cJfA==} resolution: {integrity: sha512-Dq1bqBdLaZ1Gb/l2e5/+o3B18+8TI9ANlA1SkejZqDgdU/jK/ThYaMPMJpVMMXy2uRHvGKbkz9vheVGdq3cJfA==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.19.1': '@rollup/rollup-linux-x64-musl@4.19.1':
resolution: {integrity: sha512-V7cBw/cKXMfEVhpSvVZhC+iGifD6U1zJ4tbibjjN+Xi3blSXaj/rJynAkCFFQfoG6VZrAiP7uGVzL440Q6Me2Q==} resolution: {integrity: sha512-V7cBw/cKXMfEVhpSvVZhC+iGifD6U1zJ4tbibjjN+Xi3blSXaj/rJynAkCFFQfoG6VZrAiP7uGVzL440Q6Me2Q==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-x64-musl@4.9.5': '@rollup/rollup-linux-x64-musl@4.9.5':
resolution: {integrity: sha512-ezyFUOwldYpj7AbkwyW9AJ203peub81CaAIVvckdkyH8EvhEIoKzaMFJj0G4qYJ5sw3BpqhFrsCc30t54HV8vg==} resolution: {integrity: sha512-ezyFUOwldYpj7AbkwyW9AJ203peub81CaAIVvckdkyH8EvhEIoKzaMFJj0G4qYJ5sw3BpqhFrsCc30t54HV8vg==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.19.1': '@rollup/rollup-win32-arm64-msvc@4.19.1':
resolution: {integrity: sha512-88brja2vldW/76jWATlBqHEoGjJLRnP0WOEKAUbMcXaAZnemNhlAHSyj4jIwMoP2T750LE9lblvD4e2jXleZsA==} resolution: {integrity: sha512-88brja2vldW/76jWATlBqHEoGjJLRnP0WOEKAUbMcXaAZnemNhlAHSyj4jIwMoP2T750LE9lblvD4e2jXleZsA==}
@ -4278,15 +4286,6 @@ packages:
'@types/parse5@6.0.3': '@types/parse5@6.0.3':
resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==}
'@types/passport-jwt@3.0.9':
resolution: {integrity: sha512-5XJt+79emfgpuBvBQusUPylFIVtW1QVAAkTRwCbRJAmxUjmLtIqUU6V1ovpnHPu6Qut3mR5Juc+s7kd06roNTg==}
'@types/passport-strategy@0.2.35':
resolution: {integrity: sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g==}
'@types/passport@1.0.12':
resolution: {integrity: sha512-QFdJ2TiAEoXfEQSNDISJR1Tm51I78CymqcBa8imbjo6dNNu+l2huDxxbDEIoFIwOSKMkOfHEikyDuZ38WwWsmw==}
'@types/pbf@3.0.5': '@types/pbf@3.0.5':
resolution: {integrity: sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==} resolution: {integrity: sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==}
@ -7295,6 +7294,9 @@ packages:
humanize-ms@1.2.1: humanize-ms@1.2.1:
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
i18next-browser-languagedetector@8.0.0:
resolution: {integrity: sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==}
i18next-http-backend@2.4.3: i18next-http-backend@2.4.3:
resolution: {integrity: sha512-jo2M03O6n1/DNb51WSQ8PsQ0xEELzLZRdYUTbf17mLw3rVwnJF9hwNgMXvEFSxxb+N8dT+o0vtigA6s5mGWyPA==} resolution: {integrity: sha512-jo2M03O6n1/DNb51WSQ8PsQ0xEELzLZRdYUTbf17mLw3rVwnJF9hwNgMXvEFSxxb+N8dT+o0vtigA6s5mGWyPA==}
@ -9316,17 +9318,6 @@ packages:
pascal-case@3.1.2: pascal-case@3.1.2:
resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==}
passport-jwt@4.0.1:
resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==}
passport-strategy@1.0.0:
resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==}
engines: {node: '>= 0.4.0'}
passport@0.7.0:
resolution: {integrity: sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==}
engines: {node: '>= 0.4.0'}
path-browserify@1.0.1: path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
@ -9407,9 +9398,6 @@ packages:
pathval@1.1.1: pathval@1.1.1:
resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
pause@0.0.1:
resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==}
pbf@3.2.1: pbf@3.2.1:
resolution: {integrity: sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==} resolution: {integrity: sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==}
hasBin: true hasBin: true
@ -15155,14 +15143,25 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- buffer - buffer
'@i18next-toolkit/react@1.1.0(@types/react@18.2.78)(buffer@6.0.3)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': '@i18next-toolkit/react-core@1.1.0(@types/react@18.2.78)(buffer@6.0.3)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies: dependencies:
'@types/react': 18.2.78 '@types/react': 18.2.78
crc: 4.3.2(buffer@6.0.3) crc: 4.3.2(buffer@6.0.3)
i18next: 23.10.0 i18next: 23.10.0
i18next-http-backend: 2.4.3(encoding@0.1.13)
react: 18.2.0 react: 18.2.0
react-i18next: 14.0.5(i18next@23.10.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-i18next: 14.0.5(i18next@23.10.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
transitivePeerDependencies:
- buffer
- react-dom
- react-native
'@i18next-toolkit/react@2.0.0-rc.5(@types/react@18.2.78)(buffer@6.0.3)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@i18next-toolkit/react-core': 1.1.0(@types/react@18.2.78)(buffer@6.0.3)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@types/react': 18.2.78
i18next-browser-languagedetector: 8.0.0
i18next-http-backend: 2.4.3(encoding@0.1.13)
react: 18.2.0
transitivePeerDependencies: transitivePeerDependencies:
- buffer - buffer
- encoding - encoding
@ -15298,10 +15297,6 @@ snapshots:
'@lemonsqueezy/lemonsqueezy.js@3.3.1': {} '@lemonsqueezy/lemonsqueezy.js@3.3.1': {}
'@ljharb/resumer@0.0.1':
dependencies:
'@ljharb/through': 2.3.11
'@ljharb/through@2.3.11': '@ljharb/through@2.3.11':
dependencies: dependencies:
call-bind: 1.0.7 call-bind: 1.0.7
@ -17954,21 +17949,6 @@ snapshots:
'@types/parse5@6.0.3': {} '@types/parse5@6.0.3': {}
'@types/passport-jwt@3.0.9':
dependencies:
'@types/express': 4.17.17
'@types/jsonwebtoken': 9.0.5
'@types/passport-strategy': 0.2.35
'@types/passport-strategy@0.2.35':
dependencies:
'@types/express': 4.17.17
'@types/passport': 1.0.12
'@types/passport@1.0.12':
dependencies:
'@types/express': 4.17.17
'@types/pbf@3.0.5': {} '@types/pbf@3.0.5': {}
'@types/ping@0.4.2': {} '@types/ping@0.4.2': {}
@ -21796,6 +21776,10 @@ snapshots:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
i18next-browser-languagedetector@8.0.0:
dependencies:
'@babel/runtime': 7.24.0
i18next-http-backend@2.4.3(encoding@0.1.13): i18next-http-backend@2.4.3(encoding@0.1.13):
dependencies: dependencies:
cross-fetch: 4.0.0(encoding@0.1.13) cross-fetch: 4.0.0(encoding@0.1.13)
@ -24342,19 +24326,6 @@ snapshots:
no-case: 3.0.4 no-case: 3.0.4
tslib: 2.6.2 tslib: 2.6.2
passport-jwt@4.0.1:
dependencies:
jsonwebtoken: 9.0.2
passport-strategy: 1.0.0
passport-strategy@1.0.0: {}
passport@0.7.0:
dependencies:
passport-strategy: 1.0.0
pause: 0.0.1
utils-merge: 1.0.1
path-browserify@1.0.1: {} path-browserify@1.0.1: {}
path-dirname@1.0.2: {} path-dirname@1.0.2: {}
@ -24416,8 +24387,6 @@ snapshots:
pathval@1.1.1: {} pathval@1.1.1: {}
pause@0.0.1: {}
pbf@3.2.1: pbf@3.2.1:
dependencies: dependencies:
ieee754: 1.2.1 ieee754: 1.2.1

View File

@ -14,6 +14,8 @@ import {
LuAreaChart, LuAreaChart,
LuBellDot, LuBellDot,
LuFilePieChart, LuFilePieChart,
LuKanbanSquare,
LuKeyRound,
LuMonitorDot, LuMonitorDot,
LuSearch, LuSearch,
LuServer, LuServer,
@ -171,6 +173,22 @@ export const CommandPanel: React.FC<CommandPanelProps> = React.memo((props) => {
<LuBellDot className="mr-2 h-4 w-4" /> <LuBellDot className="mr-2 h-4 w-4" />
<span>{t('Notifications')}</span> <span>{t('Notifications')}</span>
</CommandItem> </CommandItem>
<CommandItem
onSelect={handleJump({
to: '/settings/apiKey',
})}
>
<LuKeyRound className="mr-2 h-4 w-4" />
<span>{t('Api Key')}</span>
</CommandItem>
<CommandItem
onSelect={handleJump({
to: '/settings/usage',
})}
>
<LuKanbanSquare className="mr-2 h-4 w-4" />
<span>{t('Usage')}</span>
</CommandItem>
</CommandGroup> </CommandGroup>
</CommandList> </CommandList>
</Command> </Command>

View File

@ -0,0 +1,61 @@
import React from 'react';
import { Card, CardContent, CardHeader } from './ui/card';
import { formatNumber } from '@/utils/common';
import { LuAlertCircle } from 'react-icons/lu';
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
import { useTranslation } from '@i18next-toolkit/react';
import colors from 'tailwindcss/colors';
interface UsageCardProps {
title: string;
current: number;
limit?: number;
}
export const UsageCard: React.FC<UsageCardProps> = React.memo((props) => {
const { title, current, limit } = props;
const { t } = useTranslation();
return (
<Card className="relative h-full w-full overflow-hidden">
{limit && (
<div
className="absolute h-full bg-black bg-opacity-5 dark:bg-white dark:bg-opacity-10"
style={{ width: `${(current / limit) * 100}%` }}
/>
)}
{limit && current > limit && (
<div className="absolute right-2 top-2">
<Tooltip>
<TooltipTrigger>
<LuAlertCircle stroke={colors.red['500']} />
</TooltipTrigger>
<TooltipContent>
<div>
{t(
'Exceeded the limit, please upgrade your plan or your workspace will be paused soon.'
)}
</div>
</TooltipContent>
</Tooltip>
</div>
)}
<CardHeader className="text-muted-foreground">{title}</CardHeader>
<CardContent>
{limit && limit >= 0 ? (
<div>
<span className="text-2xl font-bold">{formatNumber(current)}</span>{' '}
/ <span>{formatNumber(limit)}</span>
</div>
) : (
<div>
<span className="text-2xl font-bold">{formatNumber(current)}</span>{' '}
/ <span></span>
</div>
)}
</CardContent>
</Card>
);
});
UsageCard.displayName = 'UsageCard';

View File

@ -0,0 +1,166 @@
import { Check } from 'lucide-react';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import React from 'react';
import { useTranslation } from '@i18next-toolkit/react';
import { useEvent } from '@/hooks/useEvent';
import { defaultErrorHandler, trpc } from '@/api/trpc';
import { useCurrentWorkspaceId } from '@/store/user';
import { cn } from '@/utils/style';
import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
import { LuInfo } from 'react-icons/lu';
interface SubscriptionSelectionProps {
tier: 'FREE' | 'PRO' | 'TEAM' | 'UNLIMITED' | undefined;
}
export const SubscriptionSelection: React.FC<SubscriptionSelectionProps> =
React.memo((props) => {
const { tier } = props;
const workspaceId = useCurrentWorkspaceId();
const { t } = useTranslation();
const checkoutMutation = trpc.billing.checkout.useMutation({
onError: defaultErrorHandler,
});
const handleCheckoutSubscribe = useEvent(
async (tier: 'free' | 'pro' | 'team') => {
const { url } = await checkoutMutation.mutateAsync({
workspaceId,
tier,
redirectUrl: location.href,
});
location.href = url;
}
);
const plans = [
{
id: 'FREE',
name: t('Free'),
price: 0,
features: [
t('Basic trial'),
t('Basic Usage'),
t('Up to 3 websites'),
t('Up to 3 surveys'),
t('Up to 3 feed channels'),
t('100K website events per month'),
t('100K monitor execution per month'),
t('10K feed event per month'),
t('Discord Community Support'),
],
onClick: () => handleCheckoutSubscribe('free'),
},
{
id: 'PRO',
name: 'Pro',
price: 19.99,
features: [
t('Sufficient for most situations'),
t('Priority access to advanced features'),
t('Up to 10 websites'),
t('Up to 20 surveys'),
t('Up to 20 feed channels'),
t('1M website events per month'),
t('1M monitor execution per month'),
t('100K feed events per month'),
t('Discord Community Support'),
],
onClick: () => handleCheckoutSubscribe('pro'),
},
{
id: 'TEAM',
name: 'Team',
price: 99.99,
features: [
t('Fully sufficient'),
t('Priority access to advanced features'),
t('Unlimited websites'),
t('Unlimited surveys'),
t('Unlimited feed channels'),
t('20M website events per month'),
t('20M monitor execution per month'),
t('1M feed events per month'),
t('Priority email support'),
],
onClick: () => handleCheckoutSubscribe('team'),
},
];
return (
<div className="container mx-auto px-4 py-8">
<h1 className="mb-8 text-center text-3xl font-bold">
{t('Subscription Plan')}
</h1>
<Alert className="mb-4">
<LuInfo className="h-4 w-4" />
<AlertTitle>{t('Current Plan')}</AlertTitle>
<AlertDescription>
{t('Your Current Plan is:')}{' '}
<span className="font-bold">{tier}</span>
</AlertDescription>
</Alert>
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
{plans.map((plan) => {
const isCurrent = plan.id === tier;
return (
<Card
key={plan.name}
className={cn('flex flex-col', isCurrent && 'border-primary')}
>
<CardHeader>
<CardTitle>{plan.name}</CardTitle>
<CardDescription>${plan.price} per month</CardDescription>
</CardHeader>
<CardContent className="flex-grow">
<ul className="space-y-2">
{plan.features.map((feature) => (
<li key={feature} className="flex items-center">
<Check className="mr-2 h-4 w-4 text-green-500" />
{feature}
</li>
))}
</ul>
</CardContent>
<CardFooter>
{isCurrent ? (
<Button className="w-full" disabled variant="outline">
{t('Current')}
</Button>
) : (
<Button
className="w-full"
disabled={checkoutMutation.isLoading}
onClick={plan.onClick}
>
{t('{{action}} to {{plan}}', {
action:
plans.indexOf(plan) <
plans.findIndex((p) => p.id === tier)
? t('Downgrade')
: t('Upgrade'),
plan: plan.name,
})}
</Button>
)}
</CardFooter>
</Card>
);
})}
</div>
</div>
);
});
SubscriptionSelection.displayName = 'SubscriptionSelection';

View File

@ -23,7 +23,7 @@ import {
useUserInfo, useUserInfo,
useUserStore, useUserStore,
} from '@/store/user'; } from '@/store/user';
import { languages } from '@/utils/constants'; import { languages } from '@/utils/i18n';
import { useTranslation, setLanguage } from '@i18next-toolkit/react'; import { useTranslation, setLanguage } from '@i18next-toolkit/react';
import { useNavigate } from '@tanstack/react-router'; import { useNavigate } from '@tanstack/react-router';
import { version } from '@/utils/env'; import { version } from '@/utils/env';

View File

@ -0,0 +1,59 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/utils/style"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

View File

@ -22,6 +22,9 @@ export function useGlobalConfig(): AppRouterOutput['global']['config'] {
{ {
staleTime: 1000 * 60 * 60 * 1, // 1 hour staleTime: 1000 * 60 * 60 * 1, // 1 hour
onSuccess(data) { onSuccess(data) {
/**
* Call anonymous telemetry if not disabled
*/
if (data.disableAnonymousTelemetry !== true) { if (data.disableAnonymousTelemetry !== true) {
callAnonymousTelemetry(); callAnonymousTelemetry();
} }

View File

@ -1,6 +1,6 @@
/** @type {import('@i18next-toolkit/cli').I18nextToolkitConfig} */ /** @type {import('@i18next-toolkit/cli').I18nextToolkitConfig} */
const config = { const config = {
locales: ['en', 'zh', 'jp', 'fr', 'de', 'pl', 'pt', 'ru'], locales: ['en', 'zh-CN', 'ja-JP', 'fr-FR', 'de-DE', 'pl-PL', 'pt-PT', 'ru-RU'],
verbose: true, verbose: true,
namespaces: ['translation'], namespaces: ['translation'],
translator: { translator: {

3
src/client/init.ts Normal file
View File

@ -0,0 +1,3 @@
import { initI18N } from './utils/i18n';
initI18N();

View File

@ -1,5 +1,6 @@
import './index.css'; import './index.css';
import './styles/global.less'; import './styles/global.less';
import './init';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';

View File

@ -23,7 +23,7 @@
"@bytemd/plugin-gfm": "^1.21.0", "@bytemd/plugin-gfm": "^1.21.0",
"@bytemd/react": "^1.21.0", "@bytemd/react": "^1.21.0",
"@hookform/resolvers": "^3.3.4", "@hookform/resolvers": "^3.3.4",
"@i18next-toolkit/react": "^1.1.0", "@i18next-toolkit/react": "2.0.0-rc.5",
"@loadable/component": "^5.16.3", "@loadable/component": "^5.16.3",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-alert-dialog": "^1.0.5",

View File

Before

Width:  |  Height:  |  Size: 269 B

After

Width:  |  Height:  |  Size: 269 B

View File

@ -91,6 +91,7 @@
"k3e8b13f8": "Discord beitreten", "k3e8b13f8": "Discord beitreten",
"k3eaab921": "ÜberwachungsListe", "k3eaab921": "ÜberwachungsListe",
"k3f36e17e": "Twitter folgen", "k3f36e17e": "Twitter folgen",
"k406089a4": "Aktion",
"k406e9ad8": "Bestätigen", "k406e9ad8": "Bestätigen",
"k41d3ce6c": "Ereignis wiederhergestellt", "k41d3ce6c": "Ereignis wiederhergestellt",
"k42347b91": "Website-Ereigniszählung", "k42347b91": "Website-Ereigniszählung",
@ -99,6 +100,7 @@
"k44186b66": "Zählung", "k44186b66": "Zählung",
"k44cad477": "(Aktuell)", "k44cad477": "(Aktuell)",
"k45f80a27": "Erweitert", "k45f80a27": "Erweitert",
"k4727e4db": "Ablaufdatum",
"k477b7ee4": "Teilweise Systemausfälle", "k477b7ee4": "Teilweise Systemausfälle",
"k47fe1f95": "Fügen Sie diesen Beispielcode zu Ihrem Projekt hinzu", "k47fe1f95": "Fügen Sie diesen Beispielcode zu Ihrem Projekt hinzu",
"k48186ce": "Zurück zur Startseite", "k48186ce": "Zurück zur Startseite",
@ -130,9 +132,12 @@
"k58267a45": "Quelle", "k58267a45": "Quelle",
"k58f90514": "Bot-Token", "k58f90514": "Bot-Token",
"k593cf342": "Sind Sie sicher, diesen Monitor zu löschen?", "k593cf342": "Sind Sie sicher, diesen Monitor zu löschen?",
"k5a782f4b": "Website-Anzahl",
"k5a839f71": "Betriebszeit", "k5a839f71": "Betriebszeit",
"k5b5be0d4": "Aktuelle Rolle", "k5b5be0d4": "Aktuelle Rolle",
"k5c18db28": "Statusseiteninformationen ändern", "k5c18db28": "Statusseiteninformationen ändern",
"k5d00536d": "Kopiert",
"k5d49d751": "Neuer API-Schlüssel wurde in Ihre Zwischenablage kopiert!",
"k5eb87a8b": "Start", "k5eb87a8b": "Start",
"k5ec0de4": "Für die HTTPS-Überwachung werden bei Zuweisung einer Benachrichtigungsmethode Benachrichtigungen 1, 3, 7 und 14 Tage vor Ablauf gesendet.", "k5ec0de4": "Für die HTTPS-Überwachung werden bei Zuweisung einer Benachrichtigungsmethode Benachrichtigungen 1, 3, 7 und 14 Tage vor Ablauf gesendet.",
"k5ecf04b0": "Ansicht", "k5ecf04b0": "Ansicht",
@ -223,6 +228,7 @@
"k93458b98": "Spielplatz", "k93458b98": "Spielplatz",
"k951a939a": "Akzeptierte Zählung der Website", "k951a939a": "Akzeptierte Zählung der Website",
"k95f932a": "Warten derzeit auf eine neue Anfrage vom Remote-Server", "k95f932a": "Warten derzeit auf eine neue Anfrage vom Remote-Server",
"k97b02874": "Seitenanzahl",
"k98f433ee": "Reporter herunterladen von", "k98f433ee": "Reporter herunterladen von",
"k9991c290": "Gemeinschaft", "k9991c290": "Gemeinschaft",
"k9a272ecf": "Sind das Ihre Server?", "k9a272ecf": "Sind das Ihre Server?",
@ -248,6 +254,7 @@
"ka6ee7455": "Website-ID", "ka6ee7455": "Website-ID",
"ka71c12e1": "Die beiden Passwörter stimmen nicht überein", "ka71c12e1": "Die beiden Passwörter stimmen nicht überein",
"ka765ad32": "Benachrichtigung", "ka765ad32": "Benachrichtigung",
"ka7d8617e": "Feed-Kanalanzahl",
"ka7fe5937": "Festplattenlesen/-schreiben", "ka7fe5937": "Festplattenlesen/-schreiben",
"ka8e41156": "Suche und schneller Sprung", "ka8e41156": "Suche und schneller Sprung",
"ka90bc019": "Deinstallieren", "ka90bc019": "Deinstallieren",
@ -269,6 +276,7 @@
"kb0e351e0": "Aktualisiert", "kb0e351e0": "Aktualisiert",
"kb114a2e8": "Veraltet", "kb114a2e8": "Veraltet",
"kb15a6374": "Sie können Ihre Statusseite in Ihrer eigenen Domain konfigurieren, zum Beispiel: status.beispiel.com", "kb15a6374": "Sie können Ihre Statusseite in Ihrer eigenen Domain konfigurieren, zum Beispiel: status.beispiel.com",
"kb2dded49": "Schlüssel",
"kb320aac4": "Überwacht seit {{dayNum}} Tagen", "kb320aac4": "Überwacht seit {{dayNum}} Tagen",
"kb35cde91": "Suche", "kb35cde91": "Suche",
"kb35d71ed": "ODER", "kb35d71ed": "ODER",
@ -276,6 +284,7 @@
"kb5673707": "Letzte 7 Tage", "kb5673707": "Letzte 7 Tage",
"kb659c1bc": "Zert. Ablauf", "kb659c1bc": "Zert. Ablauf",
"kb6d350b6": "Feed-Kanäle", "kb6d350b6": "Feed-Kanäle",
"kb7bf8869": "API-Schlüssel",
"kb7fa344a": "Wählen Sie einen Feed-Kanal zum Senden aus", "kb7fa344a": "Wählen Sie einen Feed-Kanal zum Senden aus",
"kb8de8c50": "BCC", "kb8de8c50": "BCC",
"kbb31d3db": "Statistikdatum", "kbb31d3db": "Statistikdatum",
@ -314,6 +323,7 @@
"kcd56f27b": "Zuletzt aktualisiert", "kcd56f27b": "Zuletzt aktualisiert",
"kcd643ef3": "Lade...", "kcd643ef3": "Lade...",
"kce77d0c1": "Zeitzone", "kce77d0c1": "Zeitzone",
"kcff78587": "Zuletzt verwendet am",
"kd005f7a8": "Alle Feeds werden entfernt", "kd005f7a8": "Alle Feeds werden entfernt",
"kd031b383": "Ansichten", "kd031b383": "Ansichten",
"kd092de58": "Aktueller Arbeitsbereich:", "kd092de58": "Aktueller Arbeitsbereich:",
@ -328,9 +338,11 @@
"kd7279fa6": "Code", "kd7279fa6": "Code",
"kd7985726": "{{num}} Benutzer", "kd7985726": "{{num}} Benutzer",
"kd92fa3e7": "Host-Name", "kd92fa3e7": "Host-Name",
"kdaa6ae2b": "Überwachungsanzahl",
"kdaff25a6": "Zeige den neuesten Wert", "kdaff25a6": "Zeige den neuesten Wert",
"kdb61adbb": "Offline verbergen", "kdb61adbb": "Offline verbergen",
"kdbadcf43": "Alle Systeme betriebsbereit", "kdbadcf43": "Alle Systeme betriebsbereit",
"kdbe222b": "API-Schlüssel",
"kdc10ee1a": "Erstellen Sie einen neuen Arbeitsbereich, um mit Teammitgliedern zusammenzuarbeiten.", "kdc10ee1a": "Erstellen Sie einen neuen Arbeitsbereich, um mit Teammitgliedern zusammenzuarbeiten.",
"kdc15c5d": "Daten", "kdc15c5d": "Daten",
"kdc1bf80e": "Url ist erforderlich", "kdc1bf80e": "Url ist erforderlich",

View File

@ -91,6 +91,7 @@
"k3e8b13f8": "Join Discord", "k3e8b13f8": "Join Discord",
"k3eaab921": "Monitor List", "k3eaab921": "Monitor List",
"k3f36e17e": "Follow Twitter", "k3f36e17e": "Follow Twitter",
"k406089a4": "Action",
"k406e9ad8": "Confirm", "k406e9ad8": "Confirm",
"k41d3ce6c": "Event unarchived", "k41d3ce6c": "Event unarchived",
"k42347b91": "Website Event Count", "k42347b91": "Website Event Count",
@ -99,6 +100,7 @@
"k44186b66": "Count", "k44186b66": "Count",
"k44cad477": "(Current)", "k44cad477": "(Current)",
"k45f80a27": "Advanced", "k45f80a27": "Advanced",
"k4727e4db": "Expired At",
"k477b7ee4": "Partial System Outage", "k477b7ee4": "Partial System Outage",
"k47fe1f95": "Add this example code into your project", "k47fe1f95": "Add this example code into your project",
"k48186ce": "Back to Homepage", "k48186ce": "Back to Homepage",
@ -130,9 +132,12 @@
"k58267a45": "Source", "k58267a45": "Source",
"k58f90514": "Bot Token", "k58f90514": "Bot Token",
"k593cf342": "Are you sure you want to delete this monitor?", "k593cf342": "Are you sure you want to delete this monitor?",
"k5a782f4b": "Website Count",
"k5a839f71": "Uptime", "k5a839f71": "Uptime",
"k5b5be0d4": "Current Role", "k5b5be0d4": "Current Role",
"k5c18db28": "Modify Status Page Info", "k5c18db28": "Modify Status Page Info",
"k5d00536d": "Copied",
"k5d49d751": "New api key has been copied into your clipboard!",
"k5eb87a8b": "Start", "k5eb87a8b": "Start",
"k5ec0de4": "For HTTPS monitoring, if any notification method is assigned, notifications will be sent at 1, 3, 7 and 14 days before expiration.", "k5ec0de4": "For HTTPS monitoring, if any notification method is assigned, notifications will be sent at 1, 3, 7 and 14 days before expiration.",
"k5ecf04b0": "View", "k5ecf04b0": "View",
@ -223,6 +228,7 @@
"k93458b98": "Playground", "k93458b98": "Playground",
"k951a939a": "Website Accepted Count", "k951a939a": "Website Accepted Count",
"k95f932a": "Currently waiting for a new request from the remote server", "k95f932a": "Currently waiting for a new request from the remote server",
"k97b02874": "Page Count",
"k98f433ee": "Download reporter from", "k98f433ee": "Download reporter from",
"k9991c290": "Community", "k9991c290": "Community",
"k9a272ecf": "Is this your servers?", "k9a272ecf": "Is this your servers?",
@ -248,6 +254,7 @@
"ka6ee7455": "Website ID", "ka6ee7455": "Website ID",
"ka71c12e1": "The two passwords are not consistent", "ka71c12e1": "The two passwords are not consistent",
"ka765ad32": "Notification", "ka765ad32": "Notification",
"ka7d8617e": "Feed Channel Count",
"ka7fe5937": "Disk read/write", "ka7fe5937": "Disk read/write",
"ka8e41156": "Search and quick jump", "ka8e41156": "Search and quick jump",
"ka90bc019": "Uninstall", "ka90bc019": "Uninstall",
@ -269,6 +276,7 @@
"kb0e351e0": "Refreshed", "kb0e351e0": "Refreshed",
"kb114a2e8": "Deprecated", "kb114a2e8": "Deprecated",
"kb15a6374": "You can config your status page in your own domain, for example: status.example.com", "kb15a6374": "You can config your status page in your own domain, for example: status.example.com",
"kb2dded49": "Key",
"kb320aac4": "Monitored for {{dayNum}} days", "kb320aac4": "Monitored for {{dayNum}} days",
"kb35cde91": "Search", "kb35cde91": "Search",
"kb35d71ed": "OR", "kb35d71ed": "OR",
@ -276,6 +284,7 @@
"kb5673707": "Last 7 days", "kb5673707": "Last 7 days",
"kb659c1bc": "Cert Exp.", "kb659c1bc": "Cert Exp.",
"kb6d350b6": "Feed Channels", "kb6d350b6": "Feed Channels",
"kb7bf8869": "Api Keys",
"kb7fa344a": "Select Feed Channel for send", "kb7fa344a": "Select Feed Channel for send",
"kb8de8c50": "BCC", "kb8de8c50": "BCC",
"kbb31d3db": "Statistic Date", "kbb31d3db": "Statistic Date",
@ -314,6 +323,7 @@
"kcd56f27b": "Last updated", "kcd56f27b": "Last updated",
"kcd643ef3": "Loading...", "kcd643ef3": "Loading...",
"kce77d0c1": "Timezone", "kce77d0c1": "Timezone",
"kcff78587": "Last Use At",
"kd005f7a8": "All feed will be remove", "kd005f7a8": "All feed will be remove",
"kd031b383": "Views", "kd031b383": "Views",
"kd092de58": "Current Workspace:", "kd092de58": "Current Workspace:",
@ -328,9 +338,11 @@
"kd7279fa6": "Code", "kd7279fa6": "Code",
"kd7985726": "{{num}} users", "kd7985726": "{{num}} users",
"kd92fa3e7": "Host Name", "kd92fa3e7": "Host Name",
"kdaa6ae2b": "Monitor Count",
"kdaff25a6": "Show Latest Value", "kdaff25a6": "Show Latest Value",
"kdb61adbb": "Hide Offline", "kdb61adbb": "Hide Offline",
"kdbadcf43": "All Systems Operational", "kdbadcf43": "All Systems Operational",
"kdbe222b": "Api Key",
"kdc10ee1a": "Create a new workspace to cooperate with team members.", "kdc10ee1a": "Create a new workspace to cooperate with team members.",
"kdc15c5d": "Data", "kdc15c5d": "Data",
"kdc1bf80e": "Url is required", "kdc1bf80e": "Url is required",

View File

Before

Width:  |  Height:  |  Size: 267 B

After

Width:  |  Height:  |  Size: 267 B

View File

@ -91,6 +91,7 @@
"k3e8b13f8": "Rejoindre Discord", "k3e8b13f8": "Rejoindre Discord",
"k3eaab921": "Liste de surveillance", "k3eaab921": "Liste de surveillance",
"k3f36e17e": "Suivre Twitter", "k3f36e17e": "Suivre Twitter",
"k406089a4": "Action",
"k406e9ad8": "Confirmer", "k406e9ad8": "Confirmer",
"k41d3ce6c": "Événement désarchivé", "k41d3ce6c": "Événement désarchivé",
"k42347b91": "Nombre d'événements sur le site Web", "k42347b91": "Nombre d'événements sur le site Web",
@ -99,6 +100,7 @@
"k44186b66": "Compte", "k44186b66": "Compte",
"k44cad477": "(Actuel)", "k44cad477": "(Actuel)",
"k45f80a27": "Avancé", "k45f80a27": "Avancé",
"k4727e4db": "Expiré À",
"k477b7ee4": "Panne partielle du système", "k477b7ee4": "Panne partielle du système",
"k47fe1f95": "Ajoutez ce code d'exemple à votre projet", "k47fe1f95": "Ajoutez ce code d'exemple à votre projet",
"k48186ce": "Retour à la page d'accueil", "k48186ce": "Retour à la page d'accueil",
@ -130,9 +132,12 @@
"k58267a45": "Source", "k58267a45": "Source",
"k58f90514": "Jeton de bot", "k58f90514": "Jeton de bot",
"k593cf342": "Êtes-vous sûr de vouloir supprimer ce moniteur ?", "k593cf342": "Êtes-vous sûr de vouloir supprimer ce moniteur ?",
"k5a782f4b": "Nombre de Sites Web",
"k5a839f71": "Disponibilité", "k5a839f71": "Disponibilité",
"k5b5be0d4": "Rôle actuel", "k5b5be0d4": "Rôle actuel",
"k5c18db28": "Modifier les informations de la page d'état", "k5c18db28": "Modifier les informations de la page d'état",
"k5d00536d": "Copié",
"k5d49d751": "La nouvelle clé API a été copiée dans votre presse-papiers !",
"k5eb87a8b": "Démarrer", "k5eb87a8b": "Démarrer",
"k5ec0de4": "Pour la surveillance HTTPS, si une méthode de notification est assignée, des notifications seront envoyées à 1, 3, 7 et 14 jours avant l'expiration.", "k5ec0de4": "Pour la surveillance HTTPS, si une méthode de notification est assignée, des notifications seront envoyées à 1, 3, 7 et 14 jours avant l'expiration.",
"k5ecf04b0": "Vue", "k5ecf04b0": "Vue",
@ -223,6 +228,7 @@
"k93458b98": "Terrain de jeu", "k93458b98": "Terrain de jeu",
"k951a939a": "Compte accepté par le site Web", "k951a939a": "Compte accepté par le site Web",
"k95f932a": "En attente d'une nouvelle requête du serveur distant", "k95f932a": "En attente d'une nouvelle requête du serveur distant",
"k97b02874": "Nombre de Pages",
"k98f433ee": "Télécharger le rapporteur de", "k98f433ee": "Télécharger le rapporteur de",
"k9991c290": "Communauté", "k9991c290": "Communauté",
"k9a272ecf": "S'agit-il de vos serveurs ?", "k9a272ecf": "S'agit-il de vos serveurs ?",
@ -248,6 +254,7 @@
"ka6ee7455": "ID du site Web", "ka6ee7455": "ID du site Web",
"ka71c12e1": "Les deux mots de passe ne sont pas cohérents", "ka71c12e1": "Les deux mots de passe ne sont pas cohérents",
"ka765ad32": "Notification", "ka765ad32": "Notification",
"ka7d8617e": "Nombre de Canaux de Flux",
"ka7fe5937": "Lecture/écriture de disque", "ka7fe5937": "Lecture/écriture de disque",
"ka8e41156": "Rechercher et sauter rapidement", "ka8e41156": "Rechercher et sauter rapidement",
"ka90bc019": "Désinstaller", "ka90bc019": "Désinstaller",
@ -269,6 +276,7 @@
"kb0e351e0": "Rafraîchi", "kb0e351e0": "Rafraîchi",
"kb114a2e8": "Obsolète", "kb114a2e8": "Obsolète",
"kb15a6374": "Vous pouvez configurer votre page de statut sur votre propre domaine, par exemple : status.example.com", "kb15a6374": "Vous pouvez configurer votre page de statut sur votre propre domaine, par exemple : status.example.com",
"kb2dded49": "Clé",
"kb320aac4": "Surveillé pendant {{dayNum}} jours", "kb320aac4": "Surveillé pendant {{dayNum}} jours",
"kb35cde91": "Recherche", "kb35cde91": "Recherche",
"kb35d71ed": "OU", "kb35d71ed": "OU",
@ -276,6 +284,7 @@
"kb5673707": "7 derniers jours", "kb5673707": "7 derniers jours",
"kb659c1bc": "Expiration du cert.", "kb659c1bc": "Expiration du cert.",
"kb6d350b6": "Canaux de flux", "kb6d350b6": "Canaux de flux",
"kb7bf8869": "Clés API",
"kb7fa344a": "Sélectionner le canal de flux à envoyer", "kb7fa344a": "Sélectionner le canal de flux à envoyer",
"kb8de8c50": "CCI", "kb8de8c50": "CCI",
"kbb31d3db": "Date de statistique", "kbb31d3db": "Date de statistique",
@ -314,6 +323,7 @@
"kcd56f27b": "Dernière mise à jour", "kcd56f27b": "Dernière mise à jour",
"kcd643ef3": "Chargement...", "kcd643ef3": "Chargement...",
"kce77d0c1": "Fuseau horaire", "kce77d0c1": "Fuseau horaire",
"kcff78587": "Dernière Utilisation À",
"kd005f7a8": "Tous les flux seront supprimés", "kd005f7a8": "Tous les flux seront supprimés",
"kd031b383": "Vues", "kd031b383": "Vues",
"kd092de58": "Espace de travail actuel :", "kd092de58": "Espace de travail actuel :",
@ -328,9 +338,11 @@
"kd7279fa6": "Code", "kd7279fa6": "Code",
"kd7985726": "{{num}} utilisateurs", "kd7985726": "{{num}} utilisateurs",
"kd92fa3e7": "Nom de l'hôte", "kd92fa3e7": "Nom de l'hôte",
"kdaa6ae2b": "Nombre de Moniteurs",
"kdaff25a6": "Afficher la dernière valeur", "kdaff25a6": "Afficher la dernière valeur",
"kdb61adbb": "Masquer hors ligne", "kdb61adbb": "Masquer hors ligne",
"kdbadcf43": "Tous les systèmes opérationnels", "kdbadcf43": "Tous les systèmes opérationnels",
"kdbe222b": "Clé API",
"kdc10ee1a": "Créer un nouvel espace de travail pour coopérer avec les membres de l'équipe.", "kdc10ee1a": "Créer un nouvel espace de travail pour coopérer avec les membres de l'équipe.",
"kdc15c5d": "Données", "kdc15c5d": "Données",
"kdc1bf80e": "L'URL est requise", "kdc1bf80e": "L'URL est requise",

View File

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 450 B

View File

@ -91,6 +91,7 @@
"k3e8b13f8": "Discordに参加", "k3e8b13f8": "Discordに参加",
"k3eaab921": "モニターリスト", "k3eaab921": "モニターリスト",
"k3f36e17e": "Twitterをフォロー", "k3f36e17e": "Twitterをフォロー",
"k406089a4": "アクション",
"k406e9ad8": "確認", "k406e9ad8": "確認",
"k41d3ce6c": "イベントがアーカイブ解除されました", "k41d3ce6c": "イベントがアーカイブ解除されました",
"k42347b91": "ウェブサイトイベント数", "k42347b91": "ウェブサイトイベント数",
@ -99,6 +100,7 @@
"k44186b66": "カウント", "k44186b66": "カウント",
"k44cad477": "(現在)", "k44cad477": "(現在)",
"k45f80a27": "詳細", "k45f80a27": "詳細",
"k4727e4db": "期限切れ",
"k477b7ee4": "部分的なシステム障害", "k477b7ee4": "部分的なシステム障害",
"k47fe1f95": "このサンプルコードをプロジェクトに追加してください", "k47fe1f95": "このサンプルコードをプロジェクトに追加してください",
"k48186ce": "ホームページに戻る", "k48186ce": "ホームページに戻る",
@ -130,9 +132,12 @@
"k58267a45": "ソース", "k58267a45": "ソース",
"k58f90514": "ボットトークン", "k58f90514": "ボットトークン",
"k593cf342": "このモニターを削除してもよろしいですか?", "k593cf342": "このモニターを削除してもよろしいですか?",
"k5a782f4b": "ウェブサイト数",
"k5a839f71": "アップタイム", "k5a839f71": "アップタイム",
"k5b5be0d4": "現在の役割", "k5b5be0d4": "現在の役割",
"k5c18db28": "ステータスページ情報を変更", "k5c18db28": "ステータスページ情報を変更",
"k5d00536d": "コピー済み",
"k5d49d751": "新しいAPIキーがクリップボードにコピーされました",
"k5eb87a8b": "開始", "k5eb87a8b": "開始",
"k5ec0de4": "HTTPSモニタリングの場合、通知方法が割り当てられている場合、有効期限の1、3、7、14日前に通知が送信されます。", "k5ec0de4": "HTTPSモニタリングの場合、通知方法が割り当てられている場合、有効期限の1、3、7、14日前に通知が送信されます。",
"k5ecf04b0": "ビュー", "k5ecf04b0": "ビュー",
@ -223,6 +228,7 @@
"k93458b98": "プレイグラウンド", "k93458b98": "プレイグラウンド",
"k951a939a": "ウェブサイト承認カウント", "k951a939a": "ウェブサイト承認カウント",
"k95f932a": "現在、リモートサーバーからの新しいリクエストを待機中です", "k95f932a": "現在、リモートサーバーからの新しいリクエストを待機中です",
"k97b02874": "ページ数",
"k98f433ee": "からレポーターをダウンロード", "k98f433ee": "からレポーターをダウンロード",
"k9991c290": "コミュニティ", "k9991c290": "コミュニティ",
"k9a272ecf": "これはあなたのサーバーですか?", "k9a272ecf": "これはあなたのサーバーですか?",
@ -248,6 +254,7 @@
"ka6ee7455": "ウェブサイトID", "ka6ee7455": "ウェブサイトID",
"ka71c12e1": "2つのパスワードが一致しません", "ka71c12e1": "2つのパスワードが一致しません",
"ka765ad32": "通知", "ka765ad32": "通知",
"ka7d8617e": "フィードチャンネル数",
"ka7fe5937": "ディスク読み取り/書き込み", "ka7fe5937": "ディスク読み取り/書き込み",
"ka8e41156": "検索して素早く移動", "ka8e41156": "検索して素早く移動",
"ka90bc019": "アンインストール", "ka90bc019": "アンインストール",
@ -269,6 +276,7 @@
"kb0e351e0": "更新されました", "kb0e351e0": "更新されました",
"kb114a2e8": "非推奨", "kb114a2e8": "非推奨",
"kb15a6374": "自分のドメインでステータスページを設定できます。たとえば、status.example.com", "kb15a6374": "自分のドメインでステータスページを設定できます。たとえば、status.example.com",
"kb2dded49": "キー",
"kb320aac4": "{{dayNum}}日間監視", "kb320aac4": "{{dayNum}}日間監視",
"kb35cde91": "検索", "kb35cde91": "検索",
"kb35d71ed": "または", "kb35d71ed": "または",
@ -276,6 +284,7 @@
"kb5673707": "過去7日間", "kb5673707": "過去7日間",
"kb659c1bc": "証明書の有効期限", "kb659c1bc": "証明書の有効期限",
"kb6d350b6": "フィードチャンネル", "kb6d350b6": "フィードチャンネル",
"kb7bf8869": "APIキー",
"kb7fa344a": "送信するフィードチャンネルを選択", "kb7fa344a": "送信するフィードチャンネルを選択",
"kb8de8c50": "BCC", "kb8de8c50": "BCC",
"kbb31d3db": "統計日", "kbb31d3db": "統計日",
@ -314,6 +323,7 @@
"kcd56f27b": "最終更新", "kcd56f27b": "最終更新",
"kcd643ef3": "読み込み中...", "kcd643ef3": "読み込み中...",
"kce77d0c1": "タイムゾーン", "kce77d0c1": "タイムゾーン",
"kcff78587": "最終使用日時",
"kd005f7a8": "すべてのフィードが削除されます", "kd005f7a8": "すべてのフィードが削除されます",
"kd031b383": "ビュー", "kd031b383": "ビュー",
"kd092de58": "現在のワークスペース:", "kd092de58": "現在のワークスペース:",
@ -328,9 +338,11 @@
"kd7279fa6": "コード", "kd7279fa6": "コード",
"kd7985726": "{{num}}人のユーザー", "kd7985726": "{{num}}人のユーザー",
"kd92fa3e7": "ホスト名", "kd92fa3e7": "ホスト名",
"kdaa6ae2b": "モニター数",
"kdaff25a6": "最新値を表示", "kdaff25a6": "最新値を表示",
"kdb61adbb": "オフラインを隠す", "kdb61adbb": "オフラインを隠す",
"kdbadcf43": "すべてのシステムが稼働中", "kdbadcf43": "すべてのシステムが稼働中",
"kdbe222b": "APIキー",
"kdc10ee1a": "チームメンバーと協力するために新しいワークスペースを作成します。", "kdc10ee1a": "チームメンバーと協力するために新しいワークスペースを作成します。",
"kdc15c5d": "データ", "kdc15c5d": "データ",
"kdc1bf80e": "URLは必須です", "kdc1bf80e": "URLは必須です",

View File

Before

Width:  |  Height:  |  Size: 259 B

After

Width:  |  Height:  |  Size: 259 B

View File

@ -91,6 +91,7 @@
"k3e8b13f8": "Dołącz do Discorda", "k3e8b13f8": "Dołącz do Discorda",
"k3eaab921": "Lista monitorów", "k3eaab921": "Lista monitorów",
"k3f36e17e": "Śledź na Twitterze", "k3f36e17e": "Śledź na Twitterze",
"k406089a4": "Akcja",
"k406e9ad8": "Potwierdź", "k406e9ad8": "Potwierdź",
"k41d3ce6c": "Wydarzenie odarchiwizowane", "k41d3ce6c": "Wydarzenie odarchiwizowane",
"k42347b91": "Liczba zdarzeń na stronie internetowej", "k42347b91": "Liczba zdarzeń na stronie internetowej",
@ -99,6 +100,7 @@
"k44186b66": "Liczba", "k44186b66": "Liczba",
"k44cad477": "(Obecny)", "k44cad477": "(Obecny)",
"k45f80a27": "Zaawansowane", "k45f80a27": "Zaawansowane",
"k4727e4db": "Wygasło",
"k477b7ee4": "Częściowa awaria systemu", "k477b7ee4": "Częściowa awaria systemu",
"k47fe1f95": "Dodaj ten przykładowy kod do swojego projektu", "k47fe1f95": "Dodaj ten przykładowy kod do swojego projektu",
"k48186ce": "Powrót do strony głównej", "k48186ce": "Powrót do strony głównej",
@ -130,9 +132,12 @@
"k58267a45": "Źródło", "k58267a45": "Źródło",
"k58f90514": "Token Bota", "k58f90514": "Token Bota",
"k593cf342": "Czy na pewno chcesz usunąć ten monitor?", "k593cf342": "Czy na pewno chcesz usunąć ten monitor?",
"k5a782f4b": "Liczba stron internetowych",
"k5a839f71": "Czas działania", "k5a839f71": "Czas działania",
"k5b5be0d4": "Aktualna Rola", "k5b5be0d4": "Aktualna Rola",
"k5c18db28": "Zmień informacje na stronie statusu", "k5c18db28": "Zmień informacje na stronie statusu",
"k5d00536d": "Skopiowane",
"k5d49d751": "Nowy klucz API został skopiowany do schowka!",
"k5eb87a8b": "Wznów", "k5eb87a8b": "Wznów",
"k5ec0de4": "Dla monitorowania HTTPS, jeśli przypisana jest jakakolwiek metoda powiadamiania, powiadomienia zostaną wysłane 1, 3, 7 i 14 dni przed wygaśnięciem.", "k5ec0de4": "Dla monitorowania HTTPS, jeśli przypisana jest jakakolwiek metoda powiadamiania, powiadomienia zostaną wysłane 1, 3, 7 i 14 dni przed wygaśnięciem.",
"k5ecf04b0": "Widok", "k5ecf04b0": "Widok",
@ -223,6 +228,7 @@
"k93458b98": "Plac zabaw", "k93458b98": "Plac zabaw",
"k951a939a": "Liczba zaakceptowanych stron internetowych", "k951a939a": "Liczba zaakceptowanych stron internetowych",
"k95f932a": "Obecnie czekam na nowe żądanie z zdalnego serwera", "k95f932a": "Obecnie czekam na nowe żądanie z zdalnego serwera",
"k97b02874": "Liczba stron",
"k98f433ee": "Pobierz reporter z", "k98f433ee": "Pobierz reporter z",
"k9991c290": "Społeczność", "k9991c290": "Społeczność",
"k9a272ecf": "Czy to twoje serwery?", "k9a272ecf": "Czy to twoje serwery?",
@ -248,6 +254,7 @@
"ka6ee7455": "ID strony internetowej", "ka6ee7455": "ID strony internetowej",
"ka71c12e1": "Dwa hasła nie są zgodne", "ka71c12e1": "Dwa hasła nie są zgodne",
"ka765ad32": "Powiadomienie", "ka765ad32": "Powiadomienie",
"ka7d8617e": "Liczba kanałów feed",
"ka7fe5937": "Odczyt/zapis dysku", "ka7fe5937": "Odczyt/zapis dysku",
"ka8e41156": "Wyszukiwanie i szybkie przeskakiwanie", "ka8e41156": "Wyszukiwanie i szybkie przeskakiwanie",
"ka90bc019": "Odinstaluj", "ka90bc019": "Odinstaluj",
@ -269,6 +276,7 @@
"kb0e351e0": "Odświeżone", "kb0e351e0": "Odświeżone",
"kb114a2e8": "Przestarzałe", "kb114a2e8": "Przestarzałe",
"kb15a6374": "Możesz skonfigurować swoją stronę statusu pod własną domeną, na przykład: status.example.com", "kb15a6374": "Możesz skonfigurować swoją stronę statusu pod własną domeną, na przykład: status.example.com",
"kb2dded49": "Klucz",
"kb320aac4": "Monitorowane przez {{dayNum}} dni", "kb320aac4": "Monitorowane przez {{dayNum}} dni",
"kb35cde91": "Szukaj", "kb35cde91": "Szukaj",
"kb35d71ed": "LUB", "kb35d71ed": "LUB",
@ -276,6 +284,7 @@
"kb5673707": "Ostatnie 7 dni", "kb5673707": "Ostatnie 7 dni",
"kb659c1bc": "Wygaśnięcie certyfikatu", "kb659c1bc": "Wygaśnięcie certyfikatu",
"kb6d350b6": "Kanały feedu", "kb6d350b6": "Kanały feedu",
"kb7bf8869": "Klucze API",
"kb7fa344a": "Wybierz kanał feedu do wysłania", "kb7fa344a": "Wybierz kanał feedu do wysłania",
"kb8de8c50": "DWU", "kb8de8c50": "DWU",
"kbb31d3db": "Data statystyk", "kbb31d3db": "Data statystyk",
@ -314,6 +323,7 @@
"kcd56f27b": "Ostatnia aktualizacja", "kcd56f27b": "Ostatnia aktualizacja",
"kcd643ef3": "Ładowanie...", "kcd643ef3": "Ładowanie...",
"kce77d0c1": "Strefa czasowa", "kce77d0c1": "Strefa czasowa",
"kcff78587": "Ostatnie użycie",
"kd005f7a8": "Wszystkie kanały informacyjne zostaną usunięte", "kd005f7a8": "Wszystkie kanały informacyjne zostaną usunięte",
"kd031b383": "Odsłony", "kd031b383": "Odsłony",
"kd092de58": "Aktualna przestrzeń robocza:", "kd092de58": "Aktualna przestrzeń robocza:",
@ -328,9 +338,11 @@
"kd7279fa6": "Kod", "kd7279fa6": "Kod",
"kd7985726": "{{num}} użytkowników", "kd7985726": "{{num}} użytkowników",
"kd92fa3e7": "Nazwa hosta", "kd92fa3e7": "Nazwa hosta",
"kdaa6ae2b": "Liczba monitorów",
"kdaff25a6": "Pokaż najnowszą wartość", "kdaff25a6": "Pokaż najnowszą wartość",
"kdb61adbb": "Ukryj wyłączone", "kdb61adbb": "Ukryj wyłączone",
"kdbadcf43": "Wszystkie systemy działają", "kdbadcf43": "Wszystkie systemy działają",
"kdbe222b": "Klucz API",
"kdc10ee1a": "Utwórz nową przestrzeń roboczą, aby współpracować z członkami zespołu.", "kdc10ee1a": "Utwórz nową przestrzeń roboczą, aby współpracować z członkami zespołu.",
"kdc15c5d": "Dane", "kdc15c5d": "Dane",
"kdc1bf80e": "Url jest wymagany", "kdc1bf80e": "Url jest wymagany",

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -91,6 +91,7 @@
"k3e8b13f8": "Aderir ao Discord", "k3e8b13f8": "Aderir ao Discord",
"k3eaab921": "Lista de Monitoramento", "k3eaab921": "Lista de Monitoramento",
"k3f36e17e": "Seguir o Twitter", "k3f36e17e": "Seguir o Twitter",
"k406089a4": "Ação",
"k406e9ad8": "Confirmar", "k406e9ad8": "Confirmar",
"k41d3ce6c": "Evento desarquivado", "k41d3ce6c": "Evento desarquivado",
"k42347b91": "Contagem de eventos do sítio Web", "k42347b91": "Contagem de eventos do sítio Web",
@ -99,6 +100,7 @@
"k44186b66": "Contar", "k44186b66": "Contar",
"k44cad477": "(Atual)", "k44cad477": "(Atual)",
"k45f80a27": "Avançado", "k45f80a27": "Avançado",
"k4727e4db": "Expirado Em",
"k477b7ee4": "Interrupção Parcial do Sistema", "k477b7ee4": "Interrupção Parcial do Sistema",
"k47fe1f95": "Adicione este código de exemplo ao seu projeto", "k47fe1f95": "Adicione este código de exemplo ao seu projeto",
"k48186ce": "Voltar à página inicial", "k48186ce": "Voltar à página inicial",
@ -130,9 +132,12 @@
"k58267a45": "Tipo de Letra", "k58267a45": "Tipo de Letra",
"k58f90514": "Token de Bot", "k58f90514": "Token de Bot",
"k593cf342": "De certeza que eliminou este monitor?", "k593cf342": "De certeza que eliminou este monitor?",
"k5a782f4b": "Contagem de Sites",
"k5a839f71": "Tempo de atividade", "k5a839f71": "Tempo de atividade",
"k5b5be0d4": "Função Atual", "k5b5be0d4": "Função Atual",
"k5c18db28": "Modificar Informações da Página de Status", "k5c18db28": "Modificar Informações da Página de Status",
"k5d00536d": "Copiado",
"k5d49d751": "Nova chave de API foi copiada para sua área de transferência!",
"k5eb87a8b": "Início", "k5eb87a8b": "Início",
"k5ec0de4": "Para monitoramento HTTPS, se algum método de notificação estiver atribuído, notificações serão enviadas com 1, 3, 7 e 14 dias antes do vencimento.", "k5ec0de4": "Para monitoramento HTTPS, se algum método de notificação estiver atribuído, notificações serão enviadas com 1, 3, 7 e 14 dias antes do vencimento.",
"k5ecf04b0": "Ver", "k5ecf04b0": "Ver",
@ -223,6 +228,7 @@
"k93458b98": "Playground", "k93458b98": "Playground",
"k951a939a": "Contagem de sites aceites", "k951a939a": "Contagem de sites aceites",
"k95f932a": "Aguardando atualmente uma nova solicitação do servidor remoto", "k95f932a": "Aguardando atualmente uma nova solicitação do servidor remoto",
"k97b02874": "Contagem de Páginas",
"k98f433ee": "Descarregar repórter de", "k98f433ee": "Descarregar repórter de",
"k9991c290": "Comunidade", "k9991c290": "Comunidade",
"k9a272ecf": "Estes são os vossos servidores?", "k9a272ecf": "Estes são os vossos servidores?",
@ -248,6 +254,7 @@
"ka6ee7455": "ID do sítio Web", "ka6ee7455": "ID do sítio Web",
"ka71c12e1": "As duas palavras-passe não são consistentes", "ka71c12e1": "As duas palavras-passe não são consistentes",
"ka765ad32": "Notificação", "ka765ad32": "Notificação",
"ka7d8617e": "Contagem de Canais de Feed",
"ka7fe5937": "Leitura/escrita de disco", "ka7fe5937": "Leitura/escrita de disco",
"ka8e41156": "Pesquisa e salto rápido", "ka8e41156": "Pesquisa e salto rápido",
"ka90bc019": "Desinstalar", "ka90bc019": "Desinstalar",
@ -269,6 +276,7 @@
"kb0e351e0": "Atualizado", "kb0e351e0": "Atualizado",
"kb114a2e8": "Obsoleto", "kb114a2e8": "Obsoleto",
"kb15a6374": "Você pode configurar sua página de status em seu próprio domínio, por exemplo: status.example.com", "kb15a6374": "Você pode configurar sua página de status em seu próprio domínio, por exemplo: status.example.com",
"kb2dded49": "Chave",
"kb320aac4": "Monitorizado durante {{dayNum}} dias", "kb320aac4": "Monitorizado durante {{dayNum}} dias",
"kb35cde91": "Pesquisar", "kb35cde91": "Pesquisar",
"kb35d71ed": "OU", "kb35d71ed": "OU",
@ -276,6 +284,7 @@
"kb5673707": "Últimos 7 dias", "kb5673707": "Últimos 7 dias",
"kb659c1bc": "Exp. do certificado", "kb659c1bc": "Exp. do certificado",
"kb6d350b6": "Canais de Feed", "kb6d350b6": "Canais de Feed",
"kb7bf8869": "Chaves de API",
"kb7fa344a": "Selecione o Canal de Feed para enviar", "kb7fa344a": "Selecione o Canal de Feed para enviar",
"kb8de8c50": "CCO", "kb8de8c50": "CCO",
"kbb31d3db": "Data da estatística", "kbb31d3db": "Data da estatística",
@ -314,6 +323,7 @@
"kcd56f27b": "Última atualização", "kcd56f27b": "Última atualização",
"kcd643ef3": "Carregando...", "kcd643ef3": "Carregando...",
"kce77d0c1": "Fuso Horário", "kce77d0c1": "Fuso Horário",
"kcff78587": "Último Uso Em",
"kd005f7a8": "Todos os feeds serão removidos", "kd005f7a8": "Todos os feeds serão removidos",
"kd031b383": "Vistas", "kd031b383": "Vistas",
"kd092de58": "Espaço de Trabalho Atual:", "kd092de58": "Espaço de Trabalho Atual:",
@ -328,9 +338,11 @@
"kd7279fa6": "Código", "kd7279fa6": "Código",
"kd7985726": "{{num}} utilizadores", "kd7985726": "{{num}} utilizadores",
"kd92fa3e7": "Nome do anfitrião", "kd92fa3e7": "Nome do anfitrião",
"kdaa6ae2b": "Contagem de Monitores",
"kdaff25a6": "Mostrar valor mais recente", "kdaff25a6": "Mostrar valor mais recente",
"kdb61adbb": "Ocultar offline", "kdb61adbb": "Ocultar offline",
"kdbadcf43": "Todos os Sistemas Operacionais", "kdbadcf43": "Todos os Sistemas Operacionais",
"kdbe222b": "Chave de API",
"kdc10ee1a": "Crie um novo espaço de trabalho para cooperar com os membros da equipe.", "kdc10ee1a": "Crie um novo espaço de trabalho para cooperar com os membros da equipe.",
"kdc15c5d": "Dados", "kdc15c5d": "Dados",
"kdc1bf80e": "Url é obrigatório", "kdc1bf80e": "Url é obrigatório",

View File

Before

Width:  |  Height:  |  Size: 268 B

After

Width:  |  Height:  |  Size: 268 B

View File

@ -91,6 +91,7 @@
"k3e8b13f8": "Присоединяйтесь к Discord", "k3e8b13f8": "Присоединяйтесь к Discord",
"k3eaab921": "Список мониторинга", "k3eaab921": "Список мониторинга",
"k3f36e17e": "Подписаться на Twitter", "k3f36e17e": "Подписаться на Twitter",
"k406089a4": "Действие",
"k406e9ad8": "Подтвердить", "k406e9ad8": "Подтвердить",
"k41d3ce6c": "Событие восстановлено", "k41d3ce6c": "Событие восстановлено",
"k42347b91": "Количество событий на сайте", "k42347b91": "Количество событий на сайте",
@ -99,6 +100,7 @@
"k44186b66": "Количество", "k44186b66": "Количество",
"k44cad477": "(Текущий)", "k44cad477": "(Текущий)",
"k45f80a27": "Расширенный", "k45f80a27": "Расширенный",
"k4727e4db": "Истекает",
"k477b7ee4": "Частичный сбой системы", "k477b7ee4": "Частичный сбой системы",
"k47fe1f95": "Добавьте этот пример кода в ваш проект", "k47fe1f95": "Добавьте этот пример кода в ваш проект",
"k48186ce": "Вернуться на главную страницу", "k48186ce": "Вернуться на главную страницу",
@ -130,9 +132,12 @@
"k58267a45": "Источник", "k58267a45": "Источник",
"k58f90514": "Токен бота", "k58f90514": "Токен бота",
"k593cf342": "Вы уверены, что хотите удалить этот монитор?", "k593cf342": "Вы уверены, что хотите удалить этот монитор?",
"k5a782f4b": "Количество сайтов",
"k5a839f71": "Время работы", "k5a839f71": "Время работы",
"k5b5be0d4": "Текущая роль", "k5b5be0d4": "Текущая роль",
"k5c18db28": "Изменить информацию на странице статуса", "k5c18db28": "Изменить информацию на странице статуса",
"k5d00536d": "Скопировано",
"k5d49d751": "Новый API-ключ скопирован в буфер обмена!",
"k5eb87a8b": "Старт", "k5eb87a8b": "Старт",
"k5ec0de4": "Для мониторинга HTTPS, если назначен любой метод уведомления, уведомления будут отправлены за 1, 3, 7 и 14 дней до истечения срока действия.", "k5ec0de4": "Для мониторинга HTTPS, если назначен любой метод уведомления, уведомления будут отправлены за 1, 3, 7 и 14 дней до истечения срока действия.",
"k5ecf04b0": "Просмотр", "k5ecf04b0": "Просмотр",
@ -223,6 +228,7 @@
"k93458b98": "Площадка", "k93458b98": "Площадка",
"k951a939a": "Количество принятых сайтом", "k951a939a": "Количество принятых сайтом",
"k95f932a": "В настоящее время ожидает нового запроса от удаленного сервера", "k95f932a": "В настоящее время ожидает нового запроса от удаленного сервера",
"k97b02874": "Количество страниц",
"k98f433ee": "Скачать репортер с", "k98f433ee": "Скачать репортер с",
"k9991c290": "Сообщество", "k9991c290": "Сообщество",
"k9a272ecf": "Это ваши серверы?", "k9a272ecf": "Это ваши серверы?",
@ -248,6 +254,7 @@
"ka6ee7455": "ID веб-сайта", "ka6ee7455": "ID веб-сайта",
"ka71c12e1": "Два пароля не совпадают", "ka71c12e1": "Два пароля не совпадают",
"ka765ad32": "Уведомления", "ka765ad32": "Уведомления",
"ka7d8617e": "Количество каналов ленты",
"ka7fe5937": "Чтение/запись на диск", "ka7fe5937": "Чтение/запись на диск",
"ka8e41156": "Поиск и быстрый переход", "ka8e41156": "Поиск и быстрый переход",
"ka90bc019": "Удалить", "ka90bc019": "Удалить",
@ -269,6 +276,7 @@
"kb0e351e0": "Обновлено", "kb0e351e0": "Обновлено",
"kb114a2e8": "Устаревший", "kb114a2e8": "Устаревший",
"kb15a6374": "Вы можете настроить свою страницу статуса на своем собственном домене, например: status.example.com", "kb15a6374": "Вы можете настроить свою страницу статуса на своем собственном домене, например: status.example.com",
"kb2dded49": "Ключ",
"kb320aac4": "Мониторинг в течение {{dayNum}} дней", "kb320aac4": "Мониторинг в течение {{dayNum}} дней",
"kb35cde91": "Поиск", "kb35cde91": "Поиск",
"kb35d71ed": "ИЛИ", "kb35d71ed": "ИЛИ",
@ -276,6 +284,7 @@
"kb5673707": "Последние 7 дней", "kb5673707": "Последние 7 дней",
"kb659c1bc": "Истечение серт.", "kb659c1bc": "Истечение серт.",
"kb6d350b6": "Каналы обратной связи", "kb6d350b6": "Каналы обратной связи",
"kb7bf8869": "API-ключи",
"kb7fa344a": "Выберите канал обратной связи для отправки", "kb7fa344a": "Выберите канал обратной связи для отправки",
"kb8de8c50": "Скрытая копия", "kb8de8c50": "Скрытая копия",
"kbb31d3db": "Дата статистики", "kbb31d3db": "Дата статистики",
@ -314,6 +323,7 @@
"kcd56f27b": "Последнее обновление", "kcd56f27b": "Последнее обновление",
"kcd643ef3": "Загрузка...", "kcd643ef3": "Загрузка...",
"kce77d0c1": "Часовой пояс", "kce77d0c1": "Часовой пояс",
"kcff78587": "Последнее использование",
"kd005f7a8": "Все ленты будут удалены", "kd005f7a8": "Все ленты будут удалены",
"kd031b383": "Просмотры", "kd031b383": "Просмотры",
"kd092de58": "Текущее рабочее пространство:", "kd092de58": "Текущее рабочее пространство:",
@ -328,9 +338,11 @@
"kd7279fa6": "Код", "kd7279fa6": "Код",
"kd7985726": "{{num}} пользователей", "kd7985726": "{{num}} пользователей",
"kd92fa3e7": "Имя хоста", "kd92fa3e7": "Имя хоста",
"kdaa6ae2b": "Количество мониторов",
"kdaff25a6": "Показать последнее значение", "kdaff25a6": "Показать последнее значение",
"kdb61adbb": "Скрыть офлайн", "kdb61adbb": "Скрыть офлайн",
"kdbadcf43": "Все системы работают", "kdbadcf43": "Все системы работают",
"kdbe222b": "API-ключ",
"kdc10ee1a": "Создайте новое рабочее пространство для сотрудничества с членами команды.", "kdc10ee1a": "Создайте новое рабочее пространство для сотрудничества с членами команды.",
"kdc15c5d": "Данные", "kdc15c5d": "Данные",
"kdc1bf80e": "URL обязателен", "kdc1bf80e": "URL обязателен",

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -91,6 +91,7 @@
"k3e8b13f8": "加入 Discord", "k3e8b13f8": "加入 Discord",
"k3eaab921": "监控列表", "k3eaab921": "监控列表",
"k3f36e17e": "关注 Twitter", "k3f36e17e": "关注 Twitter",
"k406089a4": "操作",
"k406e9ad8": "确认", "k406e9ad8": "确认",
"k41d3ce6c": "事件已取消归档", "k41d3ce6c": "事件已取消归档",
"k42347b91": "网站事件计数", "k42347b91": "网站事件计数",
@ -99,6 +100,7 @@
"k44186b66": "计数", "k44186b66": "计数",
"k44cad477": "(当前)", "k44cad477": "(当前)",
"k45f80a27": "高级", "k45f80a27": "高级",
"k4727e4db": "到期时间",
"k477b7ee4": "部分系统故障", "k477b7ee4": "部分系统故障",
"k47fe1f95": "将此示例代码添加到您的项目中", "k47fe1f95": "将此示例代码添加到您的项目中",
"k48186ce": "返回首页", "k48186ce": "返回首页",
@ -130,9 +132,12 @@
"k58267a45": "源", "k58267a45": "源",
"k58f90514": "机器人令牌", "k58f90514": "机器人令牌",
"k593cf342": "您确定要删除这个监控器吗?", "k593cf342": "您确定要删除这个监控器吗?",
"k5a782f4b": "网站数量",
"k5a839f71": "正常运行时间", "k5a839f71": "正常运行时间",
"k5b5be0d4": "当前角色", "k5b5be0d4": "当前角色",
"k5c18db28": "修改状态页面信息", "k5c18db28": "修改状态页面信息",
"k5d00536d": "已复制",
"k5d49d751": "新的 API 密钥已复制到您的剪贴板!",
"k5eb87a8b": "开始", "k5eb87a8b": "开始",
"k5ec0de4": "对于 HTTPS 监控,如果分配了任何通知方法,则将在到期前 1、3、7 和 14 天发送通知。", "k5ec0de4": "对于 HTTPS 监控,如果分配了任何通知方法,则将在到期前 1、3、7 和 14 天发送通知。",
"k5ecf04b0": "查看", "k5ecf04b0": "查看",
@ -223,6 +228,7 @@
"k93458b98": "游乐场", "k93458b98": "游乐场",
"k951a939a": "网站接受计数", "k951a939a": "网站接受计数",
"k95f932a": "当前正在等待来自远程服务器的新请求", "k95f932a": "当前正在等待来自远程服务器的新请求",
"k97b02874": "页面数量",
"k98f433ee": "从这里下载报告器", "k98f433ee": "从这里下载报告器",
"k9991c290": "社区", "k9991c290": "社区",
"k9a272ecf": "这是您的服务器吗?", "k9a272ecf": "这是您的服务器吗?",
@ -248,6 +254,7 @@
"ka6ee7455": "网站ID", "ka6ee7455": "网站ID",
"ka71c12e1": "两次密码不一致", "ka71c12e1": "两次密码不一致",
"ka765ad32": "通知", "ka765ad32": "通知",
"ka7d8617e": "Feed 渠道数量",
"ka7fe5937": "磁盘读/写", "ka7fe5937": "磁盘读/写",
"ka8e41156": "搜索和快速跳转", "ka8e41156": "搜索和快速跳转",
"ka90bc019": "卸载", "ka90bc019": "卸载",
@ -269,6 +276,7 @@
"kb0e351e0": "已刷新", "kb0e351e0": "已刷新",
"kb114a2e8": "已弃用", "kb114a2e8": "已弃用",
"kb15a6374": "您可以在自己的域名中配置您的状态页面例如status.example.com", "kb15a6374": "您可以在自己的域名中配置您的状态页面例如status.example.com",
"kb2dded49": "密钥",
"kb320aac4": "已监控{{dayNum}}天", "kb320aac4": "已监控{{dayNum}}天",
"kb35cde91": "搜索", "kb35cde91": "搜索",
"kb35d71ed": "或", "kb35d71ed": "或",
@ -276,6 +284,7 @@
"kb5673707": "最近7天", "kb5673707": "最近7天",
"kb659c1bc": "证书到期", "kb659c1bc": "证书到期",
"kb6d350b6": "馈送频道", "kb6d350b6": "馈送频道",
"kb7bf8869": "API 密钥",
"kb7fa344a": "选择要发送的馈送频道", "kb7fa344a": "选择要发送的馈送频道",
"kb8de8c50": "密送", "kb8de8c50": "密送",
"kbb31d3db": "统计日期", "kbb31d3db": "统计日期",
@ -314,6 +323,7 @@
"kcd56f27b": "最后更新", "kcd56f27b": "最后更新",
"kcd643ef3": "加载中...", "kcd643ef3": "加载中...",
"kce77d0c1": "时区", "kce77d0c1": "时区",
"kcff78587": "最后使用时间",
"kd005f7a8": "所有订阅将被删除", "kd005f7a8": "所有订阅将被删除",
"kd031b383": "视图", "kd031b383": "视图",
"kd092de58": "当前工作区:", "kd092de58": "当前工作区:",
@ -328,9 +338,11 @@
"kd7279fa6": "代码", "kd7279fa6": "代码",
"kd7985726": "{{num}}个用户", "kd7985726": "{{num}}个用户",
"kd92fa3e7": "主机名", "kd92fa3e7": "主机名",
"kdaa6ae2b": "监控数量",
"kdaff25a6": "显示最新值", "kdaff25a6": "显示最新值",
"kdb61adbb": "隐藏离线", "kdb61adbb": "隐藏离线",
"kdbadcf43": "所有系统正常运行", "kdbadcf43": "所有系统正常运行",
"kdbe222b": "API 密钥",
"kdc10ee1a": "创建一个新的工作区以与团队成员合作。", "kdc10ee1a": "创建一个新的工作区以与团队成员合作。",
"kdc15c5d": "数据", "kdc15c5d": "数据",
"kdc1bf80e": "网址是必需的", "kdc1bf80e": "网址是必需的",

View File

@ -34,6 +34,7 @@ import { Route as SettingsWorkspaceImport } from './routes/settings/workspace'
import { Route as SettingsUsageImport } from './routes/settings/usage' import { Route as SettingsUsageImport } from './routes/settings/usage'
import { Route as SettingsProfileImport } from './routes/settings/profile' import { Route as SettingsProfileImport } from './routes/settings/profile'
import { Route as SettingsNotificationsImport } from './routes/settings/notifications' import { Route as SettingsNotificationsImport } from './routes/settings/notifications'
import { Route as SettingsBillingImport } from './routes/settings/billing'
import { Route as SettingsAuditLogImport } from './routes/settings/auditLog' import { Route as SettingsAuditLogImport } from './routes/settings/auditLog'
import { Route as SettingsApiKeyImport } from './routes/settings/apiKey' import { Route as SettingsApiKeyImport } from './routes/settings/apiKey'
import { Route as PageAddImport } from './routes/page/add' import { Route as PageAddImport } from './routes/page/add'
@ -167,6 +168,11 @@ const SettingsNotificationsRoute = SettingsNotificationsImport.update({
getParentRoute: () => SettingsRoute, getParentRoute: () => SettingsRoute,
} as any) } as any)
const SettingsBillingRoute = SettingsBillingImport.update({
path: '/billing',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsAuditLogRoute = SettingsAuditLogImport.update({ const SettingsAuditLogRoute = SettingsAuditLogImport.update({
path: '/auditLog', path: '/auditLog',
getParentRoute: () => SettingsRoute, getParentRoute: () => SettingsRoute,
@ -326,6 +332,10 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SettingsAuditLogImport preLoaderRoute: typeof SettingsAuditLogImport
parentRoute: typeof SettingsImport parentRoute: typeof SettingsImport
} }
'/settings/billing': {
preLoaderRoute: typeof SettingsBillingImport
parentRoute: typeof SettingsImport
}
'/settings/notifications': { '/settings/notifications': {
preLoaderRoute: typeof SettingsNotificationsImport preLoaderRoute: typeof SettingsNotificationsImport
parentRoute: typeof SettingsImport parentRoute: typeof SettingsImport
@ -423,6 +433,7 @@ export const routeTree = rootRoute.addChildren([
SettingsRoute.addChildren([ SettingsRoute.addChildren([
SettingsApiKeyRoute, SettingsApiKeyRoute,
SettingsAuditLogRoute, SettingsAuditLogRoute,
SettingsBillingRoute,
SettingsNotificationsRoute, SettingsNotificationsRoute,
SettingsProfileRoute, SettingsProfileRoute,
SettingsUsageRoute, SettingsUsageRoute,

View File

@ -2,6 +2,7 @@ import { CommonHeader } from '@/components/CommonHeader';
import { CommonList } from '@/components/CommonList'; import { CommonList } from '@/components/CommonList';
import { CommonWrapper } from '@/components/CommonWrapper'; import { CommonWrapper } from '@/components/CommonWrapper';
import { Layout } from '@/components/layout'; import { Layout } from '@/components/layout';
import { useGlobalConfig } from '@/hooks/useConfig';
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { import {
@ -9,6 +10,7 @@ import {
useNavigate, useNavigate,
useRouterState, useRouterState,
} from '@tanstack/react-router'; } from '@tanstack/react-router';
import { compact } from 'lodash-es';
import { useEffect } from 'react'; import { useEffect } from 'react';
export const Route = createFileRoute('/settings')({ export const Route = createFileRoute('/settings')({
@ -22,8 +24,9 @@ function PageComponent() {
const pathname = useRouterState({ const pathname = useRouterState({
select: (state) => state.location.pathname, select: (state) => state.location.pathname,
}); });
const { enableBilling } = useGlobalConfig();
const items = [ const items = compact([
{ {
id: 'profile', id: 'profile',
title: t('Profile'), title: t('Profile'),
@ -54,7 +57,12 @@ function PageComponent() {
title: t('Usage'), title: t('Usage'),
href: '/settings/usage', href: '/settings/usage',
}, },
]; enableBilling && {
id: 'billing',
title: t('Billing'),
href: '/settings/billing',
},
]);
useEffect(() => { useEffect(() => {
if (pathname === Route.fullPath) { if (pathname === Route.fullPath) {

View File

@ -2,11 +2,13 @@ import { routeAuthBeforeLoad } from '@/utils/route';
import { createFileRoute } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { CommonWrapper } from '@/components/CommonWrapper'; import { CommonWrapper } from '@/components/CommonWrapper';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Empty, List } from 'antd';
import { useMemo, useRef } from 'react'; import { useMemo, useRef } from 'react';
import { trpc } from '../../api/trpc'; import {
import { useCurrentWorkspaceId } from '../../store/user'; defaultErrorHandler,
defaultSuccessHandler,
trpc,
} from '../../api/trpc';
import { useCurrentWorkspaceId, useHasAdminPermission } from '../../store/user';
import { CommonHeader } from '@/components/CommonHeader'; import { CommonHeader } from '@/components/CommonHeader';
import { last } from 'lodash-es'; import { last } from 'lodash-es';
import { useVirtualizer } from '@tanstack/react-virtual'; import { useVirtualizer } from '@tanstack/react-virtual';
@ -14,6 +16,9 @@ import { useWatch } from '@/hooks/useWatch';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { ColorTag } from '@/components/ColorTag'; import { ColorTag } from '@/components/ColorTag';
import { SimpleVirtualList } from '@/components/SimpleVirtualList'; import { SimpleVirtualList } from '@/components/SimpleVirtualList';
import { Button } from '@/components/ui/button';
import { LuTrash2 } from 'react-icons/lu';
import { AlertConfirm } from '@/components/AlertConfirm';
export const Route = createFileRoute('/settings/auditLog')({ export const Route = createFileRoute('/settings/auditLog')({
beforeLoad: routeAuthBeforeLoad, beforeLoad: routeAuthBeforeLoad,
@ -24,8 +29,9 @@ function PageComponent() {
const { t } = useTranslation(); const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId(); const workspaceId = useCurrentWorkspaceId();
const parentRef = useRef<HTMLDivElement>(null); const parentRef = useRef<HTMLDivElement>(null);
const hasAdminPermission = useHasAdminPermission();
const { data, hasNextPage, fetchNextPage, isFetchingNextPage } = const { data, hasNextPage, fetchNextPage, isFetchingNextPage, refetch } =
trpc.auditLog.fetchByCursor.useInfiniteQuery( trpc.auditLog.fetchByCursor.useInfiniteQuery(
{ {
workspaceId, workspaceId,
@ -35,6 +41,11 @@ function PageComponent() {
} }
); );
const clearMutation = trpc.auditLog.clear.useMutation({
onSuccess: defaultSuccessHandler,
onError: defaultErrorHandler,
});
const allData = useMemo(() => { const allData = useMemo(() => {
if (!data) { if (!data) {
return []; return [];
@ -69,7 +80,27 @@ function PageComponent() {
}); });
return ( return (
<CommonWrapper header={<CommonHeader title={t('Audit Log')} />}> <CommonWrapper
header={
<CommonHeader
title={t('Audit Log')}
actions={
<>
{hasAdminPermission && (
<AlertConfirm
onConfirm={() => {
clearMutation.mutateAsync({ workspaceId });
refetch();
}}
>
<Button variant="outline" size="icon" Icon={LuTrash2} />
</AlertConfirm>
)}
</>
}
/>
}
>
<div className="h-full overflow-hidden p-4"> <div className="h-full overflow-hidden p-4">
<SimpleVirtualList <SimpleVirtualList
allData={allData} allData={allData}

View File

@ -0,0 +1,124 @@
import { routeAuthBeforeLoad } from '@/utils/route';
import { createFileRoute } from '@tanstack/react-router';
import { useTranslation } from '@i18next-toolkit/react';
import { CommonWrapper } from '@/components/CommonWrapper';
import { ScrollArea } from '@/components/ui/scroll-area';
import { useMemo } from 'react';
import {
defaultErrorHandler,
defaultSuccessHandler,
trpc,
} from '../../api/trpc';
import { useCurrentWorkspace, useCurrentWorkspaceId } from '../../store/user';
import { CommonHeader } from '@/components/CommonHeader';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import dayjs from 'dayjs';
import { formatNumber } from '@/utils/common';
import { UsageCard } from '@/components/UsageCard';
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import { useEvent } from '@/hooks/useEvent';
import { SubscriptionSelection } from '@/components/billing/SubscriptionSelection';
export const Route = createFileRoute('/settings/billing')({
beforeLoad: routeAuthBeforeLoad,
component: PageComponent,
});
function PageComponent() {
const workspaceId = useCurrentWorkspaceId();
const { t } = useTranslation();
const { data: currentTier } = trpc.billing.currentTier.useQuery({
workspaceId,
});
const checkoutMutation = trpc.billing.checkout.useMutation({
onError: defaultErrorHandler,
});
const changePlanMutation = trpc.billing.changePlan.useMutation({
onSuccess: defaultSuccessHandler,
onError: defaultErrorHandler,
});
const cancelSubscriptionMutation =
trpc.billing.cancelSubscription.useMutation({
onSuccess: defaultSuccessHandler,
onError: defaultErrorHandler,
});
const { data, refetch, isInitialLoading, isLoading } =
trpc.billing.currentSubscription.useQuery({
workspaceId,
});
const handleChangeSubscribe = useEvent(
async (tier: 'free' | 'pro' | 'team') => {
await changePlanMutation.mutateAsync({
workspaceId,
tier,
});
refetch();
}
);
const plan = data ? (
<div className="flex flex-col gap-2">
<div className="flex gap-2">
<Button
loading={changePlanMutation.isLoading}
onClick={() => handleChangeSubscribe('free')}
>
Change plan to Free
</Button>
<Button
loading={changePlanMutation.isLoading}
onClick={() => handleChangeSubscribe('pro')}
>
Change plan to Pro
</Button>
<Button
loading={changePlanMutation.isLoading}
onClick={() => handleChangeSubscribe('team')}
>
Change plan to Team
</Button>
</div>
<div>
<Button
loading={cancelSubscriptionMutation.isLoading}
onClick={() =>
cancelSubscriptionMutation.mutateAsync({
workspaceId,
})
}
>
Cancel
</Button>
</div>
</div>
) : (
<div className="flex gap-2">
<SubscriptionSelection tier={currentTier} />
</div>
);
return (
<CommonWrapper header={<CommonHeader title={t('Billing')} />}>
<ScrollArea className="h-full overflow-hidden p-4">
<div className="flex flex-col gap-2">
<div>
<div>Current: {JSON.stringify(data)}</div>
<Button loading={isLoading} onClick={() => refetch()}>
Refresh
</Button>
</div>
<Separator className="my-2" />
{isInitialLoading === false && plan}
</div>
</ScrollArea>
</CommonWrapper>
);
}

View File

@ -10,6 +10,7 @@ import { CommonHeader } from '@/components/CommonHeader';
import { Card, CardContent, CardHeader } from '@/components/ui/card'; import { Card, CardContent, CardHeader } from '@/components/ui/card';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { formatNumber } from '@/utils/common'; import { formatNumber } from '@/utils/common';
import { UsageCard } from '@/components/UsageCard';
export const Route = createFileRoute('/settings/usage')({ export const Route = createFileRoute('/settings/usage')({
beforeLoad: routeAuthBeforeLoad, beforeLoad: routeAuthBeforeLoad,
@ -24,12 +25,20 @@ function PageComponent() {
[] []
); );
const { data } = trpc.billing.usage.useQuery({ const { data: serviceCountData } = trpc.workspace.getServiceCount.useQuery({
workspaceId,
});
const { data: billingUsageData } = trpc.billing.usage.useQuery({
workspaceId, workspaceId,
startAt: startDate.valueOf(), startAt: startDate.valueOf(),
endAt: endDate.valueOf(), endAt: endDate.valueOf(),
}); });
const { data: limit } = trpc.billing.limit.useQuery({
workspaceId,
});
return ( return (
<CommonWrapper header={<CommonHeader title={t('Usage')} />}> <CommonWrapper header={<CommonHeader title={t('Usage')} />}>
<ScrollArea className="h-full overflow-hidden p-4"> <ScrollArea className="h-full overflow-hidden p-4">
@ -45,50 +54,61 @@ function PageComponent() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3"> <div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
<Card className="flex-1"> <UsageCard
<CardHeader className="text-muted-foreground"> title={t('Website Count')}
{t('Website Accepted Count')} current={serviceCountData?.website ?? 0}
</CardHeader> limit={limit?.maxWebsiteCount}
<CardContent> />
{formatNumber(data?.websiteAcceptedCount ?? 0)}
</CardContent>
</Card>
<Card className="flex-1"> <UsageCard
<CardHeader className="text-muted-foreground"> title={t('Monitor Count')}
{t('Website Event Count')} current={serviceCountData?.monitor ?? 0}
</CardHeader> />
<CardContent>
{formatNumber(data?.websiteEventCount ?? 0)}
</CardContent>
</Card>
<Card className="flex-1"> <UsageCard
<CardHeader className="text-muted-foreground"> title={t('Survey Count')}
{t('Monitor Execution Count')} current={serviceCountData?.survey ?? 0}
</CardHeader> />
<CardContent>
{formatNumber(data?.monitorExecutionCount ?? 0)}
</CardContent>
</Card>
<Card className="flex-1"> <UsageCard
<CardHeader className="text-muted-foreground"> title={t('Page Count')}
{t('Survey Count')} current={serviceCountData?.page ?? 0}
</CardHeader> />
<CardContent>
{formatNumber(data?.surveyCount ?? 0)}
</CardContent>
</Card>
<Card className="flex-1"> <UsageCard
<CardHeader className="text-muted-foreground"> title={t('Feed Channel Count')}
{t('Feed Event Count')} current={serviceCountData?.feed ?? 0}
</CardHeader> limit={limit?.maxFeedChannelCount}
<CardContent> />
{formatNumber(data?.feedEventCount ?? 0)}
</CardContent> <UsageCard
</Card> title={t('Website Accepted Count')}
current={billingUsageData?.websiteAcceptedCount ?? 0}
/>
<UsageCard
title={t('Website Event Count')}
current={billingUsageData?.websiteEventCount ?? 0}
limit={limit?.maxWebsiteEventCount}
/>
<UsageCard
title={t('Monitor Execution Count')}
current={billingUsageData?.monitorExecutionCount ?? 0}
limit={limit?.maxMonitorExecutionCount}
/>
<UsageCard
title={t('Survey Count')}
current={billingUsageData?.surveyCount ?? 0}
limit={limit?.maxSurveyCount}
/>
<UsageCard
title={t('Feed Event Count')}
current={billingUsageData?.feedEventCount ?? 0}
limit={limit?.maxFeedEventCount}
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -1,3 +1,5 @@
import { setupI18nInstance } from '@i18next-toolkit/react';
export const languages = [ export const languages = [
{ {
label: 'English', label: 'English',
@ -5,31 +7,36 @@ export const languages = [
}, },
{ {
label: 'Deutsch', label: 'Deutsch',
key: 'de', key: 'de-DE',
}, },
{ {
label: 'Français', label: 'Français',
key: 'fr', key: 'fr-FR',
}, },
{ {
label: '日本語', label: '日本語',
key: 'jp', key: 'ja-JP',
}, },
{ {
label: 'Polski', label: 'Polski',
key: 'pl', key: 'pl-PL',
}, },
{ {
label: 'Português', label: 'Português',
key: 'pt', key: 'pt-PT',
}, },
{ {
label: 'Русский', label: 'Русский',
key: 'ru', key: 'ru-RU',
}, },
{ {
label: '简体中文', label: '简体中文',
key: 'zh', key: 'zh-CN',
}, },
]; ];
export function initI18N() {
setupI18nInstance({
supportedLngs: languages.map((l) => l.key),
});
}

View File

@ -2,7 +2,6 @@ import express from 'express';
import 'express-async-errors'; import 'express-async-errors';
import compression from 'compression'; import compression from 'compression';
import swaggerUI from 'swagger-ui-express'; import swaggerUI from 'swagger-ui-express';
import passport from 'passport';
import morgan from 'morgan'; import morgan from 'morgan';
import { websiteRouter } from './router/website.js'; import { websiteRouter } from './router/website.js';
import { telemetryRouter } from './router/telemetry.js'; import { telemetryRouter } from './router/telemetry.js';
@ -38,7 +37,6 @@ app.use(
}, },
}) })
); );
app.use(passport.initialize());
app.use(morgan('tiny')); app.use(morgan('tiny'));
app.use(cors()); app.use(cors());

View File

@ -9,6 +9,7 @@ import { token } from '../model/notification/token/index.js';
import pMap from 'p-map'; import pMap from 'p-map';
import { sendFeedEventsNotify } from '../model/feed/event.js'; import { sendFeedEventsNotify } from '../model/feed/event.js';
import { get } from 'lodash-es'; import { get } from 'lodash-es';
import { checkWorkspaceUsage } from '../model/billing/cronjob.js';
type WebsiteEventCountSqlReturn = { type WebsiteEventCountSqlReturn = {
workspace_id: string; workspace_id: string;
@ -29,6 +30,10 @@ export function initCronjob() {
checkFeedEventsNotify(FeedChannelNotifyFrequency.day), checkFeedEventsNotify(FeedChannelNotifyFrequency.day),
]); ]);
if (env.billing.enable) {
await checkWorkspaceUsage();
}
logger.info('Daily cronjob completed'); logger.info('Daily cronjob completed');
} catch (err) { } catch (err) {
logger.error('Daily cronjob error:', err); logger.error('Daily cronjob error:', err);
@ -386,6 +391,9 @@ async function dailyHTTPCertCheckNotify() {
); );
} }
/**
* Check feed events notify
*/
async function checkFeedEventsNotify( async function checkFeedEventsNotify(
notifyFrequency: FeedChannelNotifyFrequency notifyFrequency: FeedChannelNotifyFrequency
) { ) {

View File

@ -1,7 +1,3 @@
import { findUser } from '../model/user.js';
import passport from 'passport';
import { Handler } from 'express';
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { jwtSecret } from '../utils/common.js'; import { jwtSecret } from '../utils/common.js';
@ -14,38 +10,6 @@ export interface JWTPayload {
role: string; role: string;
} }
passport.use(
new JwtStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: jwtSecret,
issuer: jwtIssuer,
audience: jwtAudience,
},
function (jwt_payload, done) {
findUser(jwt_payload.id)
.then((user) => {
if (user) {
done(null, user);
} else {
done(null, false);
}
})
.catch((err) => {
done(err);
});
}
)
);
passport.serializeUser(function (user: any, cb) {
cb(null, { id: user.id, username: user.username });
});
passport.deserializeUser(function (user: any, cb) {
cb(null, user);
});
export function jwtSign(payload: JWTPayload): string { export function jwtSign(payload: JWTPayload): string {
const token = jwt.sign( const token = jwt.sign(
{ {
@ -72,9 +36,3 @@ export function jwtVerify(token: string): JWTPayload {
return payload as JWTPayload; return payload as JWTPayload;
} }
export function auth(): Handler {
return passport.authenticate('jwt', {
session: false,
});
}

View File

@ -0,0 +1,65 @@
import pMap from 'p-map';
import { prisma } from '../_client.js';
import { WorkspaceSubscriptionTier } from '@prisma/client';
import { logger } from '../../utils/logger.js';
import { getTierLimit } from './limit.js';
import { getWorkspaceUsage, pauseWorkspace } from './workspace.js';
import dayjs from 'dayjs';
import { getWorkspaceServiceCount } from '../workspace.js';
/**
* Check workspace usage
* if over limit, pause workspace
*/
export async function checkWorkspaceUsage() {
logger.info('[checkWorkspaceUsage] Start run checkWorkspaceUsage');
const workspaces = await prisma.workspace.findMany({
where: {
paused: false,
},
include: {
subscription: true,
},
});
await pMap(
workspaces,
async (workspace) => {
const tier =
workspace.subscription?.tier ?? WorkspaceSubscriptionTier.FREE;
if (tier === WorkspaceSubscriptionTier.UNLIMITED) {
return;
}
const [usage, serviceCount] = await Promise.all([
getWorkspaceUsage(
workspace.id,
dayjs().startOf('month').valueOf(),
dayjs().valueOf()
),
getWorkspaceServiceCount(workspace.id),
]);
const limit = getTierLimit(tier);
const overUsage =
serviceCount.website > limit.maxWebsiteCount ||
usage.websiteEventCount > limit.maxWebsiteEventCount ||
usage.monitorExecutionCount > limit.maxMonitorExecutionCount ||
usage.websiteEventCount > limit.maxWebsiteEventCount ||
usage.surveyCount > limit.maxSurveyCount ||
serviceCount.feed > limit.maxFeedChannelCount ||
usage.feedEventCount > limit.maxFeedEventCount;
if (overUsage) {
// pause workspace
await pauseWorkspace(workspace.id);
}
},
{
concurrency: 5,
}
);
}

View File

@ -1,19 +1,22 @@
import { TierType } from './types.js'; import { WorkspaceSubscriptionTier } from '@prisma/client';
import { z } from 'zod';
interface TierLimit { export const TierLimitSchema = z.object({
maxWebsiteCount: number; maxWebsiteCount: z.number(),
maxWebsiteEventCount: number; maxWebsiteEventCount: z.number(),
maxMonitorExecutionCount: number; maxMonitorExecutionCount: z.number(),
maxSurveyCount: number; maxSurveyCount: z.number(),
maxFeedChannelCount: number; maxFeedChannelCount: z.number(),
maxFeedEventCount: number; maxFeedEventCount: z.number(),
} });
type TierLimit = z.infer<typeof TierLimitSchema>;
/** /**
* Limit, Every month * Limit, Every month
*/ */
export function getTierLimit(tier: TierType): TierLimit { export function getTierLimit(tier: WorkspaceSubscriptionTier): TierLimit {
if (tier === 'free') { if (tier === WorkspaceSubscriptionTier.FREE) {
return { return {
maxWebsiteCount: 3, maxWebsiteCount: 3,
maxWebsiteEventCount: 100_000, maxWebsiteEventCount: 100_000,
@ -24,7 +27,7 @@ export function getTierLimit(tier: TierType): TierLimit {
}; };
} }
if (tier === 'pro') { if (tier === WorkspaceSubscriptionTier.PRO) {
return { return {
maxWebsiteCount: 10, maxWebsiteCount: 10,
maxWebsiteEventCount: 1_000_000, maxWebsiteEventCount: 1_000_000,
@ -35,7 +38,7 @@ export function getTierLimit(tier: TierType): TierLimit {
}; };
} }
if (tier === 'team') { if (tier === WorkspaceSubscriptionTier.TEAM) {
return { return {
maxWebsiteCount: -1, maxWebsiteCount: -1,
maxWebsiteEventCount: 20_000_000, maxWebsiteEventCount: 20_000_000,
@ -46,6 +49,7 @@ export function getTierLimit(tier: TierType): TierLimit {
}; };
} }
// Unlimited
return { return {
maxWebsiteCount: -1, maxWebsiteCount: -1,
maxWebsiteEventCount: -1, maxWebsiteEventCount: -1,

View File

@ -1 +0,0 @@
export type TierType = 'free' | 'pro' | 'team' | 'unlimited';

View File

@ -0,0 +1,56 @@
import { WorkspaceSubscriptionTier } from '@prisma/client';
import { prisma } from '../_client.js';
export async function getWorkspaceUsage(
workspaceId: string,
startAt: number,
endAt: number
) {
const res = await prisma.workspaceDailyUsage.aggregate({
where: {
workspaceId,
date: {
gte: new Date(startAt),
lte: new Date(endAt),
},
},
_sum: {
websiteAcceptedCount: true,
websiteEventCount: true,
monitorExecutionCount: true,
surveyCount: true,
feedEventCount: true,
},
});
return {
websiteAcceptedCount: res._sum.websiteAcceptedCount ?? 0,
websiteEventCount: res._sum.websiteEventCount ?? 0,
monitorExecutionCount: res._sum.monitorExecutionCount ?? 0,
surveyCount: res._sum.surveyCount ?? 0,
feedEventCount: res._sum.feedEventCount ?? 0,
};
}
export async function getWorkspaceSubscription(
workspaceId: string
): Promise<WorkspaceSubscriptionTier> {
const subscription = await prisma.workspaceSubscription.findFirst({
where: {
workspaceId,
},
});
return subscription?.tier ?? WorkspaceSubscriptionTier.FREE;
}
export async function pauseWorkspace(workspaceId: string) {
await prisma.workspace.update({
where: {
id: workspaceId,
},
data: {
paused: true,
},
});
}

View File

@ -90,7 +90,7 @@ export async function getMonitorSummaryWithDay(
"MonitorData" "MonitorData"
WHERE WHERE
"monitorId" = ${monitorId} AND "monitorId" = ${monitorId} AND
"createdAt" >= CURRENT_DATE - INTERVAL '${beforeDay} days' "createdAt" >= CURRENT_DATE - INTERVAL '1 day' * ${beforeDay}
GROUP BY GROUP BY
DATE("createdAt") DATE("createdAt")
ORDER BY ORDER BY

View File

@ -72,3 +72,47 @@ export async function getWorkspaceWebsiteDateRange(websiteId: string) {
min: res._min.createdAt, min: res._min.createdAt,
}; };
} }
export async function getWorkspaceServiceCount(workspaceId: string) {
const [website, monitor, telemetry, page, survey, feed] = await Promise.all([
prisma.website.count({
where: {
workspaceId,
},
}),
prisma.monitor.count({
where: {
workspaceId,
},
}),
prisma.telemetry.count({
where: {
workspaceId,
},
}),
prisma.monitorStatusPage.count({
where: {
workspaceId,
},
}),
prisma.survey.count({
where: {
workspaceId,
},
}),
prisma.feedChannel.count({
where: {
workspaceId,
},
}),
]);
return {
website,
monitor,
telemetry,
page,
survey,
feed,
};
}

View File

@ -60,8 +60,7 @@
"morgan": "^1.10.0", "morgan": "^1.10.0",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"nodemailer": "^6.9.8", "nodemailer": "^6.9.8",
"passport": "^0.7.0", "p-map": "4.0.0",
"passport-jwt": "^4.0.1",
"ping": "^0.4.4", "ping": "^0.4.4",
"prom-client": "^15.1.3", "prom-client": "^15.1.3",
"puppeteer": "23.4.1", "puppeteer": "23.4.1",
@ -92,8 +91,6 @@
"@types/morgan": "^1.9.5", "@types/morgan": "^1.9.5",
"@types/node": "^18.17.12", "@types/node": "^18.17.12",
"@types/nodemailer": "^6.4.11", "@types/nodemailer": "^6.4.11",
"@types/passport": "^1.0.12",
"@types/passport-jwt": "^3.0.9",
"@types/ping": "^0.4.2", "@types/ping": "^0.4.2",
"@types/request-ip": "^0.0.38", "@types/request-ip": "^0.0.38",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
@ -101,7 +98,6 @@
"@types/tcp-ping": "^0.1.5", "@types/tcp-ping": "^0.1.5",
"@types/uuid": "^9.0.7", "@types/uuid": "^9.0.7",
"execa": "^5.1.1", "execa": "^5.1.1",
"p-map": "4.0.0",
"prisma": "5.14.0", "prisma": "5.14.0",
"prisma-json-types-generator": "3.0.3", "prisma-json-types-generator": "3.0.3",
"prisma-zod-generator": "0.8.13", "prisma-zod-generator": "0.8.13",

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Workspace" ADD COLUMN "paused" BOOLEAN NOT NULL DEFAULT false;

View File

@ -96,6 +96,7 @@ model Workspace {
/// [CommonPayload] /// [CommonPayload]
/// @zod.custom(imports.CommonPayloadSchema) /// @zod.custom(imports.CommonPayloadSchema)
settings Json @default("{}") settings Json @default("{}")
paused Boolean @default(false) // if workspace over billing, its will marked as pause and not receive and input.
createdAt DateTime @default(now()) @db.Timestamptz(6) createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6) updatedAt DateTime @updatedAt @db.Timestamptz(6)

View File

@ -20,6 +20,7 @@ export const WorkspaceModelSchema = z.object({
* [CommonPayload] * [CommonPayload]
*/ */
settings: imports.CommonPayloadSchema, settings: imports.CommonPayloadSchema,
paused: z.boolean(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
}) })

View File

@ -1,5 +1,10 @@
import { z } from 'zod'; import { z } from 'zod';
import { OpenApiMetaInfo, router, workspaceProcedure } from '../trpc.js'; import {
OpenApiMetaInfo,
router,
workspaceAdminProcedure,
workspaceProcedure,
} from '../trpc.js';
import { OPENAPI_TAG } from '../../utils/const.js'; import { OPENAPI_TAG } from '../../utils/const.js';
import { WorkspaceAuditLogModelSchema } from '../../prisma/zod/index.js'; import { WorkspaceAuditLogModelSchema } from '../../prisma/zod/index.js';
import { prisma } from '../../model/_client.js'; import { prisma } from '../../model/_client.js';
@ -46,6 +51,24 @@ export const auditLogRouter = router({
nextCursor, nextCursor,
}; };
}), }),
clear: workspaceAdminProcedure
.meta(
buildAuditLogOpenapi({
method: 'DELETE',
path: '/clear',
description: 'clear all workspace audit log',
})
)
.output(z.void())
.mutation(async ({ input }) => {
const { workspaceId } = input;
await prisma.workspaceAuditLog.deleteMany({
where: {
workspaceId,
},
});
}),
}); });
function buildAuditLogOpenapi(meta: OpenApiMetaInfo): OpenApiMeta { function buildAuditLogOpenapi(meta: OpenApiMetaInfo): OpenApiMeta {

View File

@ -16,6 +16,12 @@ import {
SubscriptionTierType, SubscriptionTierType,
} from '../../model/billing/index.js'; } from '../../model/billing/index.js';
import { LemonSqueezySubscriptionModelSchema } from '../../prisma/zod/lemonsqueezysubscription.js'; import { LemonSqueezySubscriptionModelSchema } from '../../prisma/zod/lemonsqueezysubscription.js';
import {
getWorkspaceSubscription,
getWorkspaceUsage,
} from '../../model/billing/workspace.js';
import { getTierLimit, TierLimitSchema } from '../../model/billing/limit.js';
import { WorkspaceSubscriptionTier } from '@prisma/client';
export const billingRouter = router({ export const billingRouter = router({
usage: workspaceProcedure usage: workspaceProcedure
@ -44,30 +50,36 @@ export const billingRouter = router({
.query(async ({ input }) => { .query(async ({ input }) => {
const { workspaceId, startAt, endAt } = input; const { workspaceId, startAt, endAt } = input;
const res = await prisma.workspaceDailyUsage.aggregate({ return getWorkspaceUsage(workspaceId, startAt, endAt);
where: { }),
workspaceId, limit: workspaceProcedure
date: { .meta(
gte: new Date(startAt), buildBillingOpenapi({
lte: new Date(endAt), method: 'GET',
}, path: '/limit',
}, description: 'get workspace subscription limit',
_sum: { })
websiteAcceptedCount: true, )
websiteEventCount: true, .output(TierLimitSchema)
monitorExecutionCount: true, .query(async ({ input }) => {
surveyCount: true, const { workspaceId } = input;
feedEventCount: true, const tier = await getWorkspaceSubscription(workspaceId);
},
});
return { return getTierLimit(tier);
websiteAcceptedCount: res._sum.websiteAcceptedCount ?? 0, }),
websiteEventCount: res._sum.websiteEventCount ?? 0, currentTier: workspaceProcedure
monitorExecutionCount: res._sum.monitorExecutionCount ?? 0, .meta(
surveyCount: res._sum.surveyCount ?? 0, buildBillingOpenapi({
feedEventCount: res._sum.feedEventCount ?? 0, method: 'GET',
}; path: '/currentTier',
description: 'get workspace current tier',
})
)
.output(z.nativeEnum(WorkspaceSubscriptionTier))
.query(({ input }) => {
const { workspaceId } = input;
return getWorkspaceSubscription(workspaceId);
}), }),
currentSubscription: workspaceProcedure currentSubscription: workspaceProcedure
.meta( .meta(
@ -102,7 +114,7 @@ export const billingRouter = router({
checkout: workspaceOwnerProcedure checkout: workspaceOwnerProcedure
.input( .input(
z.object({ z.object({
tier: z.string(), tier: z.enum(['free', 'pro', 'team']),
redirectUrl: z.string().optional(), redirectUrl: z.string().optional(),
}) })
) )
@ -117,7 +129,7 @@ export const billingRouter = router({
const checkout = await createCheckoutBilling( const checkout = await createCheckoutBilling(
workspaceId, workspaceId,
userId, userId,
input.tier as SubscriptionTierType, input.tier,
redirectUrl redirectUrl
); );

View File

@ -24,9 +24,10 @@ export const globalRouter = router({
disableAnonymousTelemetry: z.boolean(), disableAnonymousTelemetry: z.boolean(),
customTrackerScriptName: z.string().optional(), customTrackerScriptName: z.string().optional(),
authProvider: z.array(z.string()), authProvider: z.array(z.string()),
enableBilling: z.boolean(),
}) })
) )
.query(async ({ input }) => { .query(async () => {
return { return {
allowRegister: env.allowRegister, allowRegister: env.allowRegister,
websiteId: env.websiteId, websiteId: env.websiteId,
@ -36,6 +37,7 @@ export const globalRouter = router({
disableAnonymousTelemetry: env.disableAnonymousTelemetry, disableAnonymousTelemetry: env.disableAnonymousTelemetry,
customTrackerScriptName: env.customTrackerScriptName, customTrackerScriptName: env.customTrackerScriptName,
authProvider: env.auth.provider, authProvider: env.auth.provider,
enableBilling: env.billing.enable,
}; };
}), }),
}); });

View File

@ -28,6 +28,7 @@ import { WorkspacesOnUsersModelSchema } from '../../prisma/zod/workspacesonusers
import { monitorManager } from '../../model/monitor/index.js'; import { monitorManager } from '../../model/monitor/index.js';
import { get, merge } from 'lodash-es'; import { get, merge } from 'lodash-es';
import { promWorkspaceCounter } from '../../utils/prometheus/client.js'; import { promWorkspaceCounter } from '../../utils/prometheus/client.js';
import { getWorkspaceServiceCount } from '../../model/workspace.js';
export const workspaceRouter = router({ export const workspaceRouter = router({
create: protectProedure create: protectProedure
@ -382,39 +383,8 @@ export const workspaceRouter = router({
.query(async ({ input }) => { .query(async ({ input }) => {
const { workspaceId } = input; const { workspaceId } = input;
const [website, monitor, telemetry, page, survey, feed] = const { website, monitor, telemetry, page, survey, feed } =
await Promise.all([ await getWorkspaceServiceCount(workspaceId);
prisma.website.count({
where: {
workspaceId,
},
}),
prisma.monitor.count({
where: {
workspaceId,
},
}),
prisma.telemetry.count({
where: {
workspaceId,
},
}),
prisma.monitorStatusPage.count({
where: {
workspaceId,
},
}),
prisma.survey.count({
where: {
workspaceId,
},
}),
prisma.feedChannel.count({
where: {
workspaceId,
},
}),
]);
const server = getServerCount(workspaceId); const server = getServerCount(workspaceId);

View File

@ -47,6 +47,7 @@ export const env = {
}, },
}, },
billing: { billing: {
enable: checkEnvTrusty(process.env.ENABLE_BILLING),
lemonSqueezy: { lemonSqueezy: {
signatureSecret: process.env.LEMON_SQUEEZY_SIGNATURE_SECRET ?? '', signatureSecret: process.env.LEMON_SQUEEZY_SIGNATURE_SECRET ?? '',
apiKey: process.env.LEMON_SQUEEZY_API_KEY ?? '', apiKey: process.env.LEMON_SQUEEZY_API_KEY ?? '',

View File

@ -205,11 +205,6 @@ const config: Config = {
defer: true, defer: true,
'data-website-id': 'clopxgjr6050tqn5dzxo7pjac', 'data-website-id': 'clopxgjr6050tqn5dzxo7pjac',
}, },
{
src: 'https://plausible.io/js/script.outbound-links.js',
defer: true,
'data-domain': 'msgbyte.com',
},
], ],
}; };