Compare commits

..
236 Commits
Author SHA1 Message Date
Bot af8b05b690 feat: custom domain management, responsive server layout, and config editor integration 2026-08-31 04:41:57 +08:00
Bot 68f3e02e26 chore: merge upstream/main into admin-frontend-domain preserving custom domain and branding features 2026-08-31 02:56:31 +08:00
naiba 1cd919b6f3 Merge pull request #196 from nezhahq/dependabot/npm_and_yarn/npm-dependencies-f3755940eb 2026-08-29 08:59:52 +08:00
dependabot[bot] 95b52024b3 chore(deps): bump the npm-dependencies group with 11 updates
Bumps the npm-dependencies group with 11 updates:

| Package | From | To |
| --- | --- | --- |
| [@types/luxon](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/luxon) | `3.7.4` | `3.7.5` |
| [framer-motion](https://github.com/motiondivision/motion) | `13.1.0` | `13.1.1` |
| [i18next](https://github.com/i18next/i18next) | `26.3.6` | `26.4.0` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.32.0` | `1.34.0` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.85.0` | `7.86.0` |
| [react-i18next](https://github.com/i18next/react-i18next) | `17.0.11` | `17.0.12` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.2.0` | `26.3.0` |
| [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `19.2.4` | `19.2.5` |
| [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `6.0.5` | `6.1.0` |
| [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) | `1.79.0` | `1.80.0` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.2.1` | `8.2.2` |


Updates `@types/luxon` from 3.7.4 to 3.7.5
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/luxon)

Updates `framer-motion` from 13.1.0 to 13.1.1
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v13.1.0...v13.1.1)

Updates `i18next` from 26.3.6 to 26.4.0
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.3.6...v26.4.0)

Updates `lucide-react` from 1.32.0 to 1.34.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.34.0/packages/lucide-react)

Updates `react-hook-form` from 7.85.0 to 7.86.0
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.85.0...v7.86.0)

Updates `react-i18next` from 17.0.11 to 17.0.12
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v17.0.11...v17.0.12)

Updates `@types/node` from 26.2.0 to 26.3.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@types/react-dom` from 19.2.4 to 19.2.5
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Updates `@vitejs/plugin-react` from 6.0.5 to 6.1.0
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.1.0/packages/plugin-react)

Updates `oxlint` from 1.79.0 to 1.80.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.80.0/npm/oxlint)

Updates `vite` from 8.2.1 to 8.2.2
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.2.2/packages/vite)

---
updated-dependencies:
- dependency-name: "@types/luxon"
  dependency-version: 3.7.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: framer-motion
  dependency-version: 13.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: i18next
  dependency-version: 26.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: lucide-react
  dependency-version: 1.34.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react-hook-form
  dependency-version: 7.86.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react-i18next
  dependency-version: 17.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@types/node"
  dependency-version: 26.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@types/react-dom"
  dependency-version: 19.2.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: oxlint
  dependency-version: 1.80.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: vite
  dependency-version: 8.2.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-28 23:35:30 +00:00
dependabot[bot] 35f2a7cd4d chore(deps): bump the npm-dependencies group with 7 updates
Bumps the npm-dependencies group with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [@hookform/resolvers](https://github.com/react-hook-form/resolvers) | `5.7.1` | `5.9.1` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.31.0` | `1.32.0` |
| [react-virtuoso](https://github.com/petyosi/react-virtuoso/tree/HEAD/packages/react-virtuoso) | `4.18.11` | `4.18.12` |
| [swr](https://github.com/vercel/swr) | `2.5.0` | `2.5.1` |
| [zustand](https://github.com/pmndrs/zustand) | `5.0.14` | `5.0.15` |
| [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) | `1.78.0` | `1.79.0` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.10` | `4.1.11` |


Updates `@hookform/resolvers` from 5.7.1 to 5.9.1
- [Release notes](https://github.com/react-hook-form/resolvers/releases)
- [Commits](https://github.com/react-hook-form/resolvers/compare/v5.7.1...v5.9.1)

Updates `lucide-react` from 1.31.0 to 1.32.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.32.0/packages/lucide-react)

Updates `react-virtuoso` from 4.18.11 to 4.18.12
- [Release notes](https://github.com/petyosi/react-virtuoso/releases)
- [Changelog](https://github.com/petyosi/react-virtuoso/blob/main/packages/react-virtuoso/CHANGELOG.md)
- [Commits](https://github.com/petyosi/react-virtuoso/commits/react-virtuoso@4.18.12/packages/react-virtuoso)

Updates `swr` from 2.5.0 to 2.5.1
- [Release notes](https://github.com/vercel/swr/releases)
- [Commits](https://github.com/vercel/swr/compare/v2.5.0...v2.5.1)

Updates `zustand` from 5.0.14 to 5.0.15
- [Release notes](https://github.com/pmndrs/zustand/releases)
- [Commits](https://github.com/pmndrs/zustand/compare/v5.0.14...v5.0.15)

Updates `oxlint` from 1.78.0 to 1.79.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.79.0/npm/oxlint)

Updates `vitest` from 4.1.10 to 4.1.11
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.11/packages/vitest)

---
updated-dependencies:
- dependency-name: "@hookform/resolvers"
  dependency-version: 5.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: lucide-react
  dependency-version: 1.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react-virtuoso
  dependency-version: 4.18.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: swr
  dependency-version: 2.5.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: zustand
  dependency-version: 5.0.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: oxlint
  dependency-version: 1.79.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: vitest
  dependency-version: 4.1.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-25 07:54:03 +08:00
naiba c7368ae7dc fix: migrate tables to TanStack React Table v9 2026-08-15 05:00:47 +00:00
dependabot[bot] 76d95ebbd4 chore(deps): bump the npm-dependencies group with 10 updates (#194)
Bumps the npm-dependencies group with 10 updates:

| Package | From | To |
| --- | --- | --- |
| [@tanstack/react-table](https://github.com/TanStack/table/tree/HEAD/packages/react-table) | `8.21.3` | `9.1.2` |
| [@types/luxon](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/luxon) | `3.7.3` | `3.7.4` |
| [framer-motion](https://github.com/motiondivision/motion) | `12.43.0` | `13.1.0` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.28.0` | `1.31.0` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.84.0` | `7.85.0` |
| [sonner](https://github.com/emilkowalski/sonner) | `2.0.7` | `2.0.8` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.1.2` | `26.2.0` |
| [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) | `1.77.0` | `1.78.0` |
| [postcss](https://github.com/postcss/postcss) | `8.5.25` | `8.5.26` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.2.0` | `8.2.1` |


Updates `@tanstack/react-table` from 8.21.3 to 9.1.2
- [Release notes](https://github.com/TanStack/table/releases)
- [Changelog](https://github.com/TanStack/table/blob/main/packages/react-table/CHANGELOG.md)
- [Commits](https://github.com/TanStack/table/commits/@tanstack/react-table@9.1.2/packages/react-table)

Updates `@types/luxon` from 3.7.3 to 3.7.4
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/luxon)

Updates `framer-motion` from 12.43.0 to 13.1.0
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.43.0...v13.1.0)

Updates `lucide-react` from 1.28.0 to 1.31.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.31.0/packages/lucide-react)

Updates `react-hook-form` from 7.84.0 to 7.85.0
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.84.0...v7.85.0)

Updates `sonner` from 2.0.7 to 2.0.8
- [Release notes](https://github.com/emilkowalski/sonner/releases)
- [Commits](https://github.com/emilkowalski/sonner/compare/v2.0.7...v2.0.8)

Updates `@types/node` from 26.1.2 to 26.2.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `oxlint` from 1.77.0 to 1.78.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.78.0/npm/oxlint)

Updates `postcss` from 8.5.25 to 8.5.26
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.25...8.5.26)

Updates `vite` from 8.2.0 to 8.2.1
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.2.1/packages/vite)

---
updated-dependencies:
- dependency-name: "@tanstack/react-table"
  dependency-version: 9.1.2
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: "@types/luxon"
  dependency-version: 3.7.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: framer-motion
  dependency-version: 13.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: lucide-react
  dependency-version: 1.31.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react-hook-form
  dependency-version: 7.85.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: sonner
  dependency-version: 2.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@types/node"
  dependency-version: 26.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: oxlint
  dependency-version: 1.78.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: postcss
  dependency-version: 8.5.26
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: vite
  dependency-version: 8.2.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-15 11:27:39 +08:00
357d8beb1b Translated using Weblate (Portuguese (Brazil)) (#193)
Currently translated at 0.3% (1 of 303 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 79.8% (242 of 303 strings)




Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/zh_Hant/
Translation: Nezha/Admin frontend

Co-authored-by: AWwue-work <awwue.work@gmail.com>
Co-authored-by: ace-consultoria <erec.novais@aceconsultoria.org>
2026-08-15 09:34:42 +08:00
naiba ab8c803607 feat(terminal): improve mobile controls and paste 2026-08-12 02:26:58 +00:00
Weblate (bot)andAWwue-work d4f3f998a3 Translated using Weblate (Chinese (Traditional Han script)) (#191)
Currently translated at 79.8% (242 of 303 strings)


Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/zh_Hant/
Translation: Nezha/Admin frontend

Co-authored-by: AWwue-work <awwue.work@gmail.com>
2026-08-11 21:38:05 +08:00
naiba 4b0368820e fix(deps): restore TanStack Table v8 compatibility 2026-08-08 09:26:07 +00:00
naiba 62755086cf chore(i18n): reconnect Hosted Weblate history 2026-08-08 08:53:18 +00:00
ace-consultoria d971512cc3 Added translation using Weblate (Portuguese (Brazil)) 2026-08-08 08:53:18 +00:00
Hosted WeblateandArif Budiman 13e9a026ab Translated using Weblate (Indonesian)
Currently translated at 100.0% (301 of 301 strings)

Co-authored-by: Arif Budiman <arifpedia@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/id/
Translation: Nezha/Admin frontend
2026-08-08 08:53:18 +00:00
dependabot[bot] 6e49025667 chore(deps-dev): bump js-yaml from 4.3.0 to 4.3.1 (#190)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.3.0 to 4.3.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/4.3.1/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.3.0...4.3.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.3.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-08 09:46:16 +08:00
dependabot[bot] a02ca3ea96 chore(deps-dev): bump dompurify from 3.4.12 to 3.4.13 (#189)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.12 to 3.4.13.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.12...3.4.13)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.4.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-08 09:44:33 +08:00
dependabot[bot] debd1d0418 chore(deps): bump the npm-dependencies group with 15 updates (#188)
Bumps the npm-dependencies group with 15 updates:

| Package | From | To |
| --- | --- | --- |
| [@hookform/resolvers](https://github.com/react-hook-form/resolvers) | `5.5.7` | `5.7.1` |
| [@tanstack/react-table](https://github.com/TanStack/table/tree/HEAD/packages/react-table) | `8.21.3` | `9.0.0` |
| [@types/luxon](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/luxon) | `3.7.2` | `3.7.3` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.27.0` | `1.28.0` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.83.0` | `7.84.0` |
| [simple-icons](https://github.com/simple-icons/simple-icons) | `16.27.1` | `16.28.0` |
| [swr](https://github.com/vercel/swr) | `2.4.2` | `2.5.0` |
| [@playwright/test](https://github.com/microsoft/playwright) | `1.62.0` | `1.62.1` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.17` | `19.2.18` |
| [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `19.2.3` | `19.2.4` |
| [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `6.0.4` | `6.0.5` |
| [jsdom](https://github.com/jsdom/jsdom) | `30.0.0` | `30.0.1` |
| [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) | `1.76.0` | `1.77.0` |
| [postcss](https://github.com/postcss/postcss) | `8.5.24` | `8.5.25` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.1.5` | `8.2.0` |


Updates `@hookform/resolvers` from 5.5.7 to 5.7.1
- [Release notes](https://github.com/react-hook-form/resolvers/releases)
- [Commits](https://github.com/react-hook-form/resolvers/compare/v5.5.7...v5.7.1)

Updates `@tanstack/react-table` from 8.21.3 to 9.0.0
- [Release notes](https://github.com/TanStack/table/releases)
- [Changelog](https://github.com/TanStack/table/blob/main/packages/react-table/CHANGELOG.md)
- [Commits](https://github.com/TanStack/table/commits/@tanstack/react-table@9.0.0/packages/react-table)

Updates `@types/luxon` from 3.7.2 to 3.7.3
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/luxon)

Updates `lucide-react` from 1.27.0 to 1.28.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.28.0/packages/lucide-react)

Updates `react-hook-form` from 7.83.0 to 7.84.0
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.83.0...v7.84.0)

Updates `simple-icons` from 16.27.1 to 16.28.0
- [Release notes](https://github.com/simple-icons/simple-icons/releases)
- [Commits](https://github.com/simple-icons/simple-icons/compare/16.27.1...16.28.0)

Updates `swr` from 2.4.2 to 2.5.0
- [Release notes](https://github.com/vercel/swr/releases)
- [Commits](https://github.com/vercel/swr/compare/v2.4.2...v2.5.0)

Updates `@playwright/test` from 1.62.0 to 1.62.1
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.62.0...v1.62.1)

Updates `@types/react` from 19.2.17 to 19.2.18
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `@types/react-dom` from 19.2.3 to 19.2.4
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Updates `@vitejs/plugin-react` from 6.0.4 to 6.0.5
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.5/packages/plugin-react)

Updates `jsdom` from 30.0.0 to 30.0.1
- [Release notes](https://github.com/jsdom/jsdom/releases)
- [Commits](https://github.com/jsdom/jsdom/compare/v30.0.0...v30.0.1)

Updates `oxlint` from 1.76.0 to 1.77.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.77.0/npm/oxlint)

Updates `postcss` from 8.5.24 to 8.5.25
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.24...8.5.25)

Updates `vite` from 8.1.5 to 8.2.0
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@8.2.0/packages/vite)

---
updated-dependencies:
- dependency-name: "@hookform/resolvers"
  dependency-version: 5.7.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@tanstack/react-table"
  dependency-version: 9.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: "@types/luxon"
  dependency-version: 3.7.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: lucide-react
  dependency-version: 1.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react-hook-form
  dependency-version: 7.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: simple-icons
  dependency-version: 16.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: swr
  dependency-version: 2.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@playwright/test"
  dependency-version: 1.62.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@types/react"
  dependency-version: 19.2.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@types/react-dom"
  dependency-version: 19.2.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.0.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: jsdom
  dependency-version: 30.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: oxlint
  dependency-version: 1.77.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: postcss
  dependency-version: 8.5.25
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: vite
  dependency-version: 8.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-08 09:42:40 +08:00
dependabot[bot] 7e315c56af chore(deps-dev): bump fast-uri from 3.1.4 to 3.1.5 (#186)
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.4 to 3.1.5.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.4...v3.1.5)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-08 09:39:02 +08:00
ace-consultoria 5518f72355 Added translation using Weblate (Portuguese (Brazil)) 2026-08-06 15:29:52 +02:00
Copilot 04fbddb979 Stabilize CI runtime for updated deps and unblock Dependabot auto-format flow (#185)
* chore: apply ci and dependabot remediation changes

* chore: auto-fix linting and formatting issues

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-08-01 10:58:33 +08:00
dependabot[bot] 95b51f0209 chore(deps): bump the npm-dependencies group with 24 updates (#184)
Bumps the npm-dependencies group with 24 updates:

| Package | From | To |
| --- | --- | --- |
| [@hookform/resolvers](https://github.com/react-hook-form/resolvers) | `5.4.0` | `5.5.7` |
| [@radix-ui/react-alert-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/alert-dialog) | `1.1.22` | `1.1.23` |
| [@radix-ui/react-avatar](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/avatar) | `1.2.5` | `1.2.6` |
| [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/checkbox) | `1.3.10` | `1.3.11` |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.22` | `1.1.23` |
| [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dropdown-menu) | `2.1.23` | `2.1.24` |
| [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.14` | `2.1.15` |
| [@radix-ui/react-navigation-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/navigation-menu) | `1.2.21` | `1.2.22` |
| [@radix-ui/react-popover](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/popover) | `1.1.22` | `1.1.23` |
| [@radix-ui/react-scroll-area](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/scroll-area) | `1.2.17` | `1.2.18` |
| [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.3.6` | `2.3.7` |
| [@radix-ui/react-separator](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/separator) | `1.1.14` | `1.1.15` |
| [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) | `1.3.2` | `1.3.3` |
| [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.3.6` | `1.3.7` |
| [@radix-ui/react-tabs](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tabs) | `1.1.20` | `1.1.21` |
| [framer-motion](https://github.com/motiondivision/motion) | `12.42.2` | `12.43.0` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.26.0` | `1.27.0` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.82.0` | `7.83.0` |
| [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `7.18.1` | `7.18.2` |
| [simple-icons](https://github.com/simple-icons/simple-icons) | `16.27.0` | `16.27.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.1.1` | `26.1.2` |
| [jsdom](https://github.com/jsdom/jsdom) | `29.1.1` | `30.0.0` |
| [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) | `1.75.0` | `1.76.0` |
| [postcss](https://github.com/postcss/postcss) | `8.5.23` | `8.5.24` |


Updates `@hookform/resolvers` from 5.4.0 to 5.5.7
- [Release notes](https://github.com/react-hook-form/resolvers/releases)
- [Commits](https://github.com/react-hook-form/resolvers/compare/v5.4.0...v5.5.7)

Updates `@radix-ui/react-alert-dialog` from 1.1.22 to 1.1.23
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/alert-dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/alert-dialog)

Updates `@radix-ui/react-avatar` from 1.2.5 to 1.2.6
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/avatar/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/avatar)

Updates `@radix-ui/react-checkbox` from 1.3.10 to 1.3.11
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/checkbox/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/checkbox)

Updates `@radix-ui/react-dialog` from 1.1.22 to 1.1.23
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog)

Updates `@radix-ui/react-dropdown-menu` from 2.1.23 to 2.1.24
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dropdown-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dropdown-menu)

Updates `@radix-ui/react-label` from 2.1.14 to 2.1.15
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label)

Updates `@radix-ui/react-navigation-menu` from 1.2.21 to 1.2.22
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/navigation-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/navigation-menu)

Updates `@radix-ui/react-popover` from 1.1.22 to 1.1.23
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/popover/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/popover)

Updates `@radix-ui/react-scroll-area` from 1.2.17 to 1.2.18
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/scroll-area/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/scroll-area)

Updates `@radix-ui/react-select` from 2.3.6 to 2.3.7
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select)

Updates `@radix-ui/react-separator` from 1.1.14 to 1.1.15
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/separator/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/separator)

Updates `@radix-ui/react-slot` from 1.3.2 to 1.3.3
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot)

Updates `@radix-ui/react-switch` from 1.3.6 to 1.3.7
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

Updates `@radix-ui/react-tabs` from 1.1.20 to 1.1.21
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tabs/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tabs)

Updates `framer-motion` from 12.42.2 to 12.43.0
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.42.2...v12.43.0)

Updates `lucide-react` from 1.26.0 to 1.27.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.27.0/packages/lucide-react)

Updates `react-hook-form` from 7.82.0 to 7.83.0
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.82.0...v7.83.0)

Updates `react-router-dom` from 7.18.1 to 7.18.2
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/react-router-dom@7.18.2/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.18.2/packages/react-router-dom)

Updates `simple-icons` from 16.27.0 to 16.27.1
- [Release notes](https://github.com/simple-icons/simple-icons/releases)
- [Commits](https://github.com/simple-icons/simple-icons/compare/16.27.0...16.27.1)

Updates `@types/node` from 26.1.1 to 26.1.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `jsdom` from 29.1.1 to 30.0.0
- [Release notes](https://github.com/jsdom/jsdom/releases)
- [Commits](https://github.com/jsdom/jsdom/compare/v29.1.1...v30.0.0)

Updates `oxlint` from 1.75.0 to 1.76.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.76.0/npm/oxlint)

Updates `postcss` from 8.5.23 to 8.5.24
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.23...8.5.24)

---
updated-dependencies:
- dependency-name: "@hookform/resolvers"
  dependency-version: 5.5.7
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-alert-dialog"
  dependency-version: 1.1.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-avatar"
  dependency-version: 1.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-checkbox"
  dependency-version: 1.3.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-version: 2.1.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-navigation-menu"
  dependency-version: 1.2.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-popover"
  dependency-version: 1.1.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-scroll-area"
  dependency-version: 1.2.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.3.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-separator"
  dependency-version: 1.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-slot"
  dependency-version: 1.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-tabs"
  dependency-version: 1.1.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: framer-motion
  dependency-version: 12.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: lucide-react
  dependency-version: 1.27.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react-hook-form
  dependency-version: 7.83.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react-router-dom
  dependency-version: 7.18.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: simple-icons
  dependency-version: 16.27.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@types/node"
  dependency-version: 26.1.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: jsdom
  dependency-version: 30.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: oxlint
  dependency-version: 1.76.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: postcss
  dependency-version: 8.5.24
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-01 08:42:09 +08:00
dependabot[bot] 95246e4dca chore(deps-dev): bump fast-uri from 3.1.3 to 3.1.4 (#183) 2026-07-25 13:50:04 +08:00
dependabot[bot] 2f51aa77e7 chore(deps-dev): bump dompurify from 3.4.11 to 3.4.12 (#181) 2026-07-25 08:59:53 +08:00
dependabot[bot] 448cc33845 chore(deps): bump the npm-dependencies group with 24 updates (#182)
Bumps the npm-dependencies group with 24 updates:

| Package | From | To |
| --- | --- | --- |
| [@radix-ui/react-alert-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/alert-dialog) | `1.1.19` | `1.1.22` |
| [@radix-ui/react-avatar](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/avatar) | `1.2.2` | `1.2.5` |
| [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/checkbox) | `1.3.7` | `1.3.10` |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.19` | `1.1.22` |
| [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dropdown-menu) | `2.1.20` | `2.1.23` |
| [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.11` | `2.1.14` |
| [@radix-ui/react-navigation-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/navigation-menu) | `1.2.18` | `1.2.21` |
| [@radix-ui/react-popover](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/popover) | `1.1.19` | `1.1.22` |
| [@radix-ui/react-scroll-area](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/scroll-area) | `1.2.14` | `1.2.17` |
| [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.3.3` | `2.3.6` |
| [@radix-ui/react-separator](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/separator) | `1.1.11` | `1.1.14` |
| [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) | `1.3.0` | `1.3.2` |
| [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.3.3` | `1.3.6` |
| [@radix-ui/react-tabs](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tabs) | `1.1.17` | `1.1.20` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.25.0` | `1.26.0` |
| [react](https://github.com/react/react/tree/HEAD/packages/react) | `19.2.7` | `19.2.8` |
| [react-dom](https://github.com/react/react/tree/HEAD/packages/react-dom) | `19.2.7` | `19.2.8` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.81.0` | `7.82.0` |
| [react-i18next](https://github.com/i18next/react-i18next) | `17.0.10` | `17.0.11` |
| [simple-icons](https://github.com/simple-icons/simple-icons) | `16.26.0` | `16.27.0` |
| [@playwright/test](https://github.com/microsoft/playwright) | `1.61.1` | `1.62.0` |
| [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `6.0.3` | `6.0.4` |
| [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) | `1.74.0` | `1.75.0` |
| [postcss](https://github.com/postcss/postcss) | `8.5.19` | `8.5.23` |


Updates `@radix-ui/react-alert-dialog` from 1.1.19 to 1.1.22
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/alert-dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/alert-dialog)

Updates `@radix-ui/react-avatar` from 1.2.2 to 1.2.5
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/avatar/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/avatar)

Updates `@radix-ui/react-checkbox` from 1.3.7 to 1.3.10
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/checkbox/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/checkbox)

Updates `@radix-ui/react-dialog` from 1.1.19 to 1.1.22
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog)

Updates `@radix-ui/react-dropdown-menu` from 2.1.20 to 2.1.23
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dropdown-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dropdown-menu)

Updates `@radix-ui/react-label` from 2.1.11 to 2.1.14
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label)

Updates `@radix-ui/react-navigation-menu` from 1.2.18 to 1.2.21
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/navigation-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/navigation-menu)

Updates `@radix-ui/react-popover` from 1.1.19 to 1.1.22
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/popover/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/popover)

Updates `@radix-ui/react-scroll-area` from 1.2.14 to 1.2.17
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/scroll-area/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/scroll-area)

Updates `@radix-ui/react-select` from 2.3.3 to 2.3.6
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select)

Updates `@radix-ui/react-separator` from 1.1.11 to 1.1.14
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/separator/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/separator)

Updates `@radix-ui/react-slot` from 1.3.0 to 1.3.2
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot)

Updates `@radix-ui/react-switch` from 1.3.3 to 1.3.6
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

Updates `@radix-ui/react-tabs` from 1.1.17 to 1.1.20
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tabs/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tabs)

Updates `lucide-react` from 1.25.0 to 1.26.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.26.0/packages/lucide-react)

Updates `react` from 19.2.7 to 19.2.8
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.2.8/packages/react)

Updates `react-dom` from 19.2.7 to 19.2.8
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.2.8/packages/react-dom)

Updates `react-hook-form` from 7.81.0 to 7.82.0
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.81.0...v7.82.0)

Updates `react-i18next` from 17.0.10 to 17.0.11
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v17.0.10...v17.0.11)

Updates `simple-icons` from 16.26.0 to 16.27.0
- [Release notes](https://github.com/simple-icons/simple-icons/releases)
- [Commits](https://github.com/simple-icons/simple-icons/compare/16.26.0...16.27.0)

Updates `@playwright/test` from 1.61.1 to 1.62.0
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.61.1...v1.62.0)

Updates `@vitejs/plugin-react` from 6.0.3 to 6.0.4
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.4/packages/plugin-react)

Updates `oxlint` from 1.74.0 to 1.75.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.75.0/npm/oxlint)

Updates `postcss` from 8.5.19 to 8.5.23
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.19...8.5.23)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-alert-dialog"
  dependency-version: 1.1.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-avatar"
  dependency-version: 1.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-checkbox"
  dependency-version: 1.3.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-version: 2.1.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-navigation-menu"
  dependency-version: 1.2.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-popover"
  dependency-version: 1.1.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-scroll-area"
  dependency-version: 1.2.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-separator"
  dependency-version: 1.1.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-slot"
  dependency-version: 1.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-tabs"
  dependency-version: 1.1.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: lucide-react
  dependency-version: 1.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react
  dependency-version: 19.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: react-dom
  dependency-version: 19.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: react-hook-form
  dependency-version: 7.82.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react-i18next
  dependency-version: 17.0.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: simple-icons
  dependency-version: 16.27.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@playwright/test"
  dependency-version: 1.62.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.0.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: oxlint
  dependency-version: 1.75.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: postcss
  dependency-version: 8.5.23
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 08:59:20 +08:00
naibaandnaiba/CloudCode b9a145ff2a Merge pull request #180 from nezhahq/fix/typescript-7-lint-tooling
fix: restore CI compatibility with TypeScript 7

Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-21 18:32:15 +08:00
naibaandnaiba/CloudCode 74a89b39ef ci: run lint before frontend build
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-21 10:09:04 +00:00
naibaandnaiba/CloudCode 31c6249f40 fix: restore recommended Oxlint coverage
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-21 10:08:57 +00:00
naibaandnaiba/CloudCode feb069a7d0 chore: migrate lint configuration to Oxlint
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-21 08:58:16 +00:00
naibaandnaiba/CloudCode 5ace626f23 build: replace ESLint tooling with Oxlint
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-21 08:57:39 +00:00
naibaandnaiba/CloudCode 27ba6e74bd fix: remove obsolete TypeScript baseUrl options
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-21 08:57:13 +00:00
naiba ef98d3a13b Merge pull request #179 from nezhahq/dependabot/npm_and_yarn/npm-dependencies-4690c62d08 2026-07-18 08:27:06 +08:00
naiba 0b963b8dba Merge pull request #178 from nezhahq/dependabot/github_actions/github-actions-4c2bda7e8b 2026-07-18 08:26:47 +08:00
dependabot[bot] 37d93ec03d chore(deps): bump the npm-dependencies group with 12 updates
Bumps the npm-dependencies group with 12 updates:

| Package | From | To |
| --- | --- | --- |
| [@tailwindcss/postcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss) | `4.3.2` | `4.3.3` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.24.0` | `1.25.0` |
| [prettier-plugin-tailwindcss](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) | `0.8.0` | `0.8.1` |
| [react-i18next](https://github.com/i18next/react-i18next) | `17.0.9` | `17.0.10` |
| [react-virtuoso](https://github.com/petyosi/react-virtuoso/tree/HEAD/packages/react-virtuoso) | `4.18.10` | `4.18.11` |
| [simple-icons](https://github.com/simple-icons/simple-icons) | `16.25.0` | `16.26.0` |
| [autoprefixer](https://github.com/postcss/autoprefixer) | `10.5.2` | `10.5.4` |
| [postcss](https://github.com/postcss/postcss) | `8.5.16` | `8.5.19` |
| [swagger-typescript-api](https://github.com/acacode/swagger-typescript-api) | `13.12.4` | `13.12.6` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.3.2` | `4.3.3` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.63.0` | `8.64.0` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.1.4` | `8.1.5` |


Updates `@tailwindcss/postcss` from 4.3.2 to 4.3.3
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.3/packages/@tailwindcss-postcss)

Updates `lucide-react` from 1.24.0 to 1.25.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.25.0/packages/lucide-react)

Updates `prettier-plugin-tailwindcss` from 0.8.0 to 0.8.1
- [Release notes](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.8.0...v0.8.1)

Updates `react-i18next` from 17.0.9 to 17.0.10
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v17.0.9...v17.0.10)

Updates `react-virtuoso` from 4.18.10 to 4.18.11
- [Release notes](https://github.com/petyosi/react-virtuoso/releases)
- [Changelog](https://github.com/petyosi/react-virtuoso/blob/main/packages/react-virtuoso/CHANGELOG.md)
- [Commits](https://github.com/petyosi/react-virtuoso/commits/HEAD/packages/react-virtuoso)

Updates `simple-icons` from 16.25.0 to 16.26.0
- [Release notes](https://github.com/simple-icons/simple-icons/releases)
- [Commits](https://github.com/simple-icons/simple-icons/compare/16.25.0...16.26.0)

Updates `autoprefixer` from 10.5.2 to 10.5.4
- [Release notes](https://github.com/postcss/autoprefixer/releases)
- [Changelog](https://github.com/postcss/autoprefixer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/autoprefixer/compare/10.5.2...10.5.4)

Updates `postcss` from 8.5.16 to 8.5.19
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.16...8.5.19)

Updates `swagger-typescript-api` from 13.12.4 to 13.12.6
- [Release notes](https://github.com/acacode/swagger-typescript-api/releases)
- [Changelog](https://github.com/acacode/swagger-typescript-api/blob/main/CHANGELOG.md)
- [Commits](https://github.com/acacode/swagger-typescript-api/compare/v13.12.4...v13.12.6)

Updates `tailwindcss` from 4.3.2 to 4.3.3
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.3/packages/tailwindcss)

Updates `typescript-eslint` from 8.63.0 to 8.64.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.64.0/packages/typescript-eslint)

Updates `vite` from 8.1.4 to 8.1.5
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.1.5/packages/vite)

---
updated-dependencies:
- dependency-name: "@tailwindcss/postcss"
  dependency-version: 4.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: lucide-react
  dependency-version: 1.25.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: prettier-plugin-tailwindcss
  dependency-version: 0.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: react-i18next
  dependency-version: 17.0.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: react-virtuoso
  dependency-version: 4.18.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: simple-icons
  dependency-version: 16.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: autoprefixer
  dependency-version: 10.5.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: postcss
  dependency-version: 8.5.19
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: swagger-typescript-api
  dependency-version: 13.12.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: tailwindcss
  dependency-version: 4.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.64.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: vite
  dependency-version: 8.1.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-17 23:36:05 +00:00
dependabot[bot] a4843283a8 chore(deps): bump the github-actions group with 2 updates
Bumps the github-actions group with 2 updates: [actions/setup-node](https://github.com/actions/setup-node) and [actions/setup-go](https://github.com/actions/setup-go).


Updates `actions/setup-node` from 6 to 7
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v6...v7)

Updates `actions/setup-go` from 6 to 7
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/setup-go
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-17 23:34:18 +00:00
dependabot[bot] 8e1f6e1767 chore(deps): bump the npm-dependencies group with 22 updates (#177) 2026-07-12 20:39:20 +08:00
naibaandnaiba/CloudCode 628efb99a5 ci: add frontend build and test workflow
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-04 06:36:29 +00:00
naibaandnaiba/CloudCode 1b4572b318 Merge pull request #175 from dysf888/main
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-04 06:34:28 +00:00
naibaandnaiba/CloudCode 53d181fea7 Merge pull request #176 from dependabot/npm_and_yarn/npm-dependencies-3ca08eec11
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-04 06:33:00 +00:00
dependabot[bot] 92b7795658 chore(deps): bump the npm-dependencies group with 32 updates
Bumps the npm-dependencies group with 32 updates:

| Package | From | To |
| --- | --- | --- |
| [@radix-ui/react-alert-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/alert-dialog) | `1.1.17` | `1.1.18` |
| [@radix-ui/react-avatar](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/avatar) | `1.2.0` | `1.2.1` |
| [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/checkbox) | `1.3.5` | `1.3.6` |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.17` | `1.1.18` |
| [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dropdown-menu) | `2.1.18` | `2.1.19` |
| [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.10` | `2.1.11` |
| [@radix-ui/react-navigation-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/navigation-menu) | `1.2.16` | `1.2.17` |
| [@radix-ui/react-popover](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/popover) | `1.1.17` | `1.1.18` |
| [@radix-ui/react-scroll-area](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/scroll-area) | `1.2.12` | `1.2.13` |
| [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.3.1` | `2.3.2` |
| [@radix-ui/react-separator](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/separator) | `1.1.10` | `1.1.11` |
| [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.3.1` | `1.3.2` |
| [@radix-ui/react-tabs](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tabs) | `1.1.15` | `1.1.16` |
| [@tailwindcss/postcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss) | `4.3.1` | `4.3.2` |
| [framer-motion](https://github.com/motiondivision/motion) | `12.40.0` | `12.42.2` |
| [i18next](https://github.com/i18next/i18next) | `26.3.1` | `26.3.4` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.21.0` | `1.23.0` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.79.0` | `7.80.0` |
| [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `7.18.0` | `7.18.1` |
| [react-virtuoso](https://github.com/petyosi/react-virtuoso/tree/HEAD/packages/react-virtuoso) | `4.18.7` | `4.18.10` |
| [swr](https://github.com/vercel/swr) | `2.4.1` | `2.4.2` |
| [@playwright/test](https://github.com/microsoft/playwright) | `1.61.0` | `1.61.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.0.0` | `26.1.0` |
| [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `6.0.2` | `6.0.3` |
| [autoprefixer](https://github.com/postcss/autoprefixer) | `10.5.0` | `10.5.2` |
| [eslint](https://github.com/eslint/eslint) | `10.5.0` | `10.6.0` |
| [globals](https://github.com/sindresorhus/globals) | `17.6.0` | `17.7.0` |
| [postcss](https://github.com/postcss/postcss) | `8.5.15` | `8.5.16` |
| [swagger-typescript-api](https://github.com/acacode/swagger-typescript-api) | `13.12.2` | `13.12.4` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.3.1` | `4.3.2` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.61.1` | `8.62.1` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.0.16` | `8.1.3` |


Updates `@radix-ui/react-alert-dialog` from 1.1.17 to 1.1.18
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/alert-dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/alert-dialog)

Updates `@radix-ui/react-avatar` from 1.2.0 to 1.2.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/avatar/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/avatar)

Updates `@radix-ui/react-checkbox` from 1.3.5 to 1.3.6
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/checkbox/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/checkbox)

Updates `@radix-ui/react-dialog` from 1.1.17 to 1.1.18
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog)

Updates `@radix-ui/react-dropdown-menu` from 2.1.18 to 2.1.19
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dropdown-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dropdown-menu)

Updates `@radix-ui/react-label` from 2.1.10 to 2.1.11
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label)

Updates `@radix-ui/react-navigation-menu` from 1.2.16 to 1.2.17
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/navigation-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/navigation-menu)

Updates `@radix-ui/react-popover` from 1.1.17 to 1.1.18
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/popover/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/popover)

Updates `@radix-ui/react-scroll-area` from 1.2.12 to 1.2.13
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/scroll-area/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/scroll-area)

Updates `@radix-ui/react-select` from 2.3.1 to 2.3.2
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select)

Updates `@radix-ui/react-separator` from 1.1.10 to 1.1.11
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/separator/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/separator)

Updates `@radix-ui/react-switch` from 1.3.1 to 1.3.2
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

Updates `@radix-ui/react-tabs` from 1.1.15 to 1.1.16
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tabs/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tabs)

Updates `@tailwindcss/postcss` from 4.3.1 to 4.3.2
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.2/packages/@tailwindcss-postcss)

Updates `framer-motion` from 12.40.0 to 12.42.2
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.40.0...v12.42.2)

Updates `i18next` from 26.3.1 to 26.3.4
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.3.1...v26.3.4)

Updates `lucide-react` from 1.21.0 to 1.23.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.23.0/packages/lucide-react)

Updates `react-hook-form` from 7.79.0 to 7.80.0
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.79.0...v7.80.0)

Updates `react-router-dom` from 7.18.0 to 7.18.1
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/react-router-dom@7.18.1/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.18.1/packages/react-router-dom)

Updates `react-virtuoso` from 4.18.7 to 4.18.10
- [Release notes](https://github.com/petyosi/react-virtuoso/releases)
- [Changelog](https://github.com/petyosi/react-virtuoso/blob/main/packages/react-virtuoso/CHANGELOG.md)
- [Commits](https://github.com/petyosi/react-virtuoso/commits/react-virtuoso@4.18.10/packages/react-virtuoso)

Updates `swr` from 2.4.1 to 2.4.2
- [Release notes](https://github.com/vercel/swr/releases)
- [Commits](https://github.com/vercel/swr/compare/v2.4.1...v2.4.2)

Updates `@playwright/test` from 1.61.0 to 1.61.1
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.61.0...v1.61.1)

Updates `@types/node` from 26.0.0 to 26.1.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@vitejs/plugin-react` from 6.0.2 to 6.0.3
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.3/packages/plugin-react)

Updates `autoprefixer` from 10.5.0 to 10.5.2
- [Release notes](https://github.com/postcss/autoprefixer/releases)
- [Changelog](https://github.com/postcss/autoprefixer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/autoprefixer/compare/10.5.0...10.5.2)

Updates `eslint` from 10.5.0 to 10.6.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.5.0...v10.6.0)

Updates `globals` from 17.6.0 to 17.7.0
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v17.6.0...v17.7.0)

Updates `postcss` from 8.5.15 to 8.5.16
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.15...8.5.16)

Updates `swagger-typescript-api` from 13.12.2 to 13.12.4
- [Release notes](https://github.com/acacode/swagger-typescript-api/releases)
- [Changelog](https://github.com/acacode/swagger-typescript-api/blob/main/CHANGELOG.md)
- [Commits](https://github.com/acacode/swagger-typescript-api/compare/v13.12.2...v13.12.4)

Updates `tailwindcss` from 4.3.1 to 4.3.2
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.2/packages/tailwindcss)

Updates `typescript-eslint` from 8.61.1 to 8.62.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.62.1/packages/typescript-eslint)

Updates `vite` from 8.0.16 to 8.1.3
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.1.3/packages/vite)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-alert-dialog"
  dependency-version: 1.1.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-avatar"
  dependency-version: 1.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-checkbox"
  dependency-version: 1.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-version: 2.1.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-navigation-menu"
  dependency-version: 1.2.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-popover"
  dependency-version: 1.1.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-scroll-area"
  dependency-version: 1.2.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-separator"
  dependency-version: 1.1.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-tabs"
  dependency-version: 1.1.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@tailwindcss/postcss"
  dependency-version: 4.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: framer-motion
  dependency-version: 12.42.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: i18next
  dependency-version: 26.3.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: lucide-react
  dependency-version: 1.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react-hook-form
  dependency-version: 7.80.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react-router-dom
  dependency-version: 7.18.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: react-virtuoso
  dependency-version: 4.18.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: swr
  dependency-version: 2.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@playwright/test"
  dependency-version: 1.61.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@types/node"
  dependency-version: 26.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: autoprefixer
  dependency-version: 10.5.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: eslint
  dependency-version: 10.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: globals
  dependency-version: 17.7.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: postcss
  dependency-version: 8.5.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: swagger-typescript-api
  dependency-version: 13.12.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: tailwindcss
  dependency-version: 4.3.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.62.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: vite
  dependency-version: 8.1.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-03 23:37:28 +00:00
dysf888 64e6249e2d Harden OAuth provider icon loading 2026-07-03 11:20:21 +08:00
dysf888 6bd7866688 Add simple-icons support for OAuth providers 2026-07-03 11:10:19 +08:00
dependabot[bot] 207fadf64e chore(deps-dev): bump dompurify from 3.4.10 to 3.4.11
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.10 to 3.4.11.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.10...3.4.11)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.4.11
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-20 08:35:42 +08:00
dependabot[bot] 5e67db0e2a chore(deps): bump undici from 7.25.0 to 7.28.0 (#173) 2026-06-20 08:35:26 +08:00
naiba 839b71d0e0 Merge pull request #169 from nezhahq/dependabot/npm_and_yarn/dompurify-3.4.10 2026-06-20 08:09:04 +08:00
naiba 781983fa2d Merge pull request #172 from nezhahq/dependabot/npm_and_yarn/npm-dependencies-5b7fbd4ff8 2026-06-20 08:08:31 +08:00
dependabot[bot] 318e68c3da chore(deps): bump actions/checkout in the github-actions group
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-20 08:08:14 +08:00
dependabot[bot] c02dcb2fad chore(deps): bump the npm-dependencies group with 23 updates
Bumps the npm-dependencies group with 23 updates:

| Package | From | To |
| --- | --- | --- |
| [@radix-ui/react-alert-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/alert-dialog) | `1.1.16` | `1.1.17` |
| [@radix-ui/react-avatar](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/avatar) | `1.1.12` | `1.2.0` |
| [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/checkbox) | `1.3.4` | `1.3.5` |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.16` | `1.1.17` |
| [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dropdown-menu) | `2.1.17` | `2.1.18` |
| [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.9` | `2.1.10` |
| [@radix-ui/react-navigation-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/navigation-menu) | `1.2.15` | `1.2.16` |
| [@radix-ui/react-popover](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/popover) | `1.1.16` | `1.1.17` |
| [@radix-ui/react-scroll-area](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/scroll-area) | `1.2.11` | `1.2.12` |
| [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.3.0` | `2.3.1` |
| [@radix-ui/react-separator](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/separator) | `1.1.9` | `1.1.10` |
| [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) | `1.2.5` | `1.3.0` |
| [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.3.0` | `1.3.1` |
| [@radix-ui/react-tabs](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tabs) | `1.1.14` | `1.1.15` |
| [@types/luxon](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/luxon) | `3.7.1` | `3.7.2` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.18.0` | `1.21.0` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.78.0` | `7.79.0` |
| [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `7.17.0` | `7.18.0` |
| [@playwright/test](https://github.com/microsoft/playwright) | `1.60.0` | `1.61.0` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.9.3` | `26.0.0` |
| [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) | `0.5.2` | `0.5.3` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.61.0` | `8.61.1` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.8` | `4.1.9` |


Updates `@radix-ui/react-alert-dialog` from 1.1.16 to 1.1.17
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/alert-dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/alert-dialog)

Updates `@radix-ui/react-avatar` from 1.1.12 to 1.2.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/avatar/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/avatar)

Updates `@radix-ui/react-checkbox` from 1.3.4 to 1.3.5
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/checkbox/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/checkbox)

Updates `@radix-ui/react-dialog` from 1.1.16 to 1.1.17
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog)

Updates `@radix-ui/react-dropdown-menu` from 2.1.17 to 2.1.18
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dropdown-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dropdown-menu)

Updates `@radix-ui/react-label` from 2.1.9 to 2.1.10
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label)

Updates `@radix-ui/react-navigation-menu` from 1.2.15 to 1.2.16
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/navigation-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/navigation-menu)

Updates `@radix-ui/react-popover` from 1.1.16 to 1.1.17
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/popover/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/popover)

Updates `@radix-ui/react-scroll-area` from 1.2.11 to 1.2.12
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/scroll-area/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/scroll-area)

Updates `@radix-ui/react-select` from 2.3.0 to 2.3.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select)

Updates `@radix-ui/react-separator` from 1.1.9 to 1.1.10
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/separator/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/separator)

Updates `@radix-ui/react-slot` from 1.2.5 to 1.3.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot)

Updates `@radix-ui/react-switch` from 1.3.0 to 1.3.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

Updates `@radix-ui/react-tabs` from 1.1.14 to 1.1.15
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tabs/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tabs)

Updates `@types/luxon` from 3.7.1 to 3.7.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/luxon)

Updates `lucide-react` from 1.18.0 to 1.21.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.21.0/packages/lucide-react)

Updates `react-hook-form` from 7.78.0 to 7.79.0
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.78.0...v7.79.0)

Updates `react-router-dom` from 7.17.0 to 7.18.0
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/react-router-dom@7.18.0/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.18.0/packages/react-router-dom)

Updates `@playwright/test` from 1.60.0 to 1.61.0
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.60.0...v1.61.0)

Updates `@types/node` from 25.9.3 to 26.0.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `eslint-plugin-react-refresh` from 0.5.2 to 0.5.3
- [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases)
- [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.5.2...v0.5.3)

Updates `typescript-eslint` from 8.61.0 to 8.61.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.61.1/packages/typescript-eslint)

Updates `vitest` from 4.1.8 to 4.1.9
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.9/packages/vitest)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-alert-dialog"
  dependency-version: 1.1.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-avatar"
  dependency-version: 1.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-checkbox"
  dependency-version: 1.3.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-version: 2.1.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-navigation-menu"
  dependency-version: 1.2.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-popover"
  dependency-version: 1.1.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-scroll-area"
  dependency-version: 1.2.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-separator"
  dependency-version: 1.1.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-slot"
  dependency-version: 1.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-tabs"
  dependency-version: 1.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@types/luxon"
  dependency-version: 3.7.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: lucide-react
  dependency-version: 1.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react-hook-form
  dependency-version: 7.79.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react-router-dom
  dependency-version: 7.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@playwright/test"
  dependency-version: 1.61.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@types/node"
  dependency-version: 26.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: eslint-plugin-react-refresh
  dependency-version: 0.5.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.61.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: vitest
  dependency-version: 4.1.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-19 23:36:08 +00:00
dependabot[bot] 48c5b742b1 chore(deps-dev): bump dompurify from 3.4.8 to 3.4.10
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.8 to 3.4.10.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.8...3.4.10)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.4.10
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-17 02:34:28 +00:00
naiba 4d17b2e018 Merge pull request #167 from nezhahq/dependabot/npm_and_yarn/npm-dependencies-1adf4e205b 2026-06-17 09:40:13 +08:00
dependabot[bot] c20ba4bc66 chore(deps): bump the npm-dependencies group with 22 updates
Bumps the npm-dependencies group with 22 updates:

| Package | From | To |
| --- | --- | --- |
| [@radix-ui/react-alert-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/alert-dialog) | `1.1.15` | `1.1.16` |
| [@radix-ui/react-avatar](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/avatar) | `1.1.11` | `1.1.12` |
| [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/checkbox) | `1.3.3` | `1.3.4` |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.15` | `1.1.16` |
| [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dropdown-menu) | `2.1.16` | `2.1.17` |
| [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.8` | `2.1.9` |
| [@radix-ui/react-navigation-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/navigation-menu) | `1.2.14` | `1.2.15` |
| [@radix-ui/react-popover](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/popover) | `1.1.15` | `1.1.16` |
| [@radix-ui/react-scroll-area](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/scroll-area) | `1.2.10` | `1.2.11` |
| [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.2.6` | `2.3.0` |
| [@radix-ui/react-separator](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/separator) | `1.1.8` | `1.1.9` |
| [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) | `1.2.4` | `1.2.5` |
| [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.2.6` | `1.3.0` |
| [@radix-ui/react-tabs](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tabs) | `1.1.13` | `1.1.14` |
| [@tailwindcss/postcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss) | `4.3.0` | `4.3.1` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.17.0` | `1.18.0` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.77.0` | `7.78.0` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.9.2` | `25.9.3` |
| [eslint](https://github.com/eslint/eslint) | `10.4.1` | `10.5.0` |
| [swagger-typescript-api](https://github.com/acacode/swagger-typescript-api) | `13.12.1` | `13.12.2` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.3.0` | `4.3.1` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.60.1` | `8.61.0` |


Updates `@radix-ui/react-alert-dialog` from 1.1.15 to 1.1.16
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/alert-dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/alert-dialog)

Updates `@radix-ui/react-avatar` from 1.1.11 to 1.1.12
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/avatar/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/avatar)

Updates `@radix-ui/react-checkbox` from 1.3.3 to 1.3.4
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/checkbox/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/checkbox)

Updates `@radix-ui/react-dialog` from 1.1.15 to 1.1.16
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog)

Updates `@radix-ui/react-dropdown-menu` from 2.1.16 to 2.1.17
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dropdown-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dropdown-menu)

Updates `@radix-ui/react-label` from 2.1.8 to 2.1.9
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label)

Updates `@radix-ui/react-navigation-menu` from 1.2.14 to 1.2.15
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/navigation-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/navigation-menu)

Updates `@radix-ui/react-popover` from 1.1.15 to 1.1.16
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/popover/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/popover)

Updates `@radix-ui/react-scroll-area` from 1.2.10 to 1.2.11
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/scroll-area/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/scroll-area)

Updates `@radix-ui/react-select` from 2.2.6 to 2.3.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select)

Updates `@radix-ui/react-separator` from 1.1.8 to 1.1.9
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/separator/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/separator)

Updates `@radix-ui/react-slot` from 1.2.4 to 1.2.5
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot)

Updates `@radix-ui/react-switch` from 1.2.6 to 1.3.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

Updates `@radix-ui/react-tabs` from 1.1.13 to 1.1.14
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tabs/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tabs)

Updates `@tailwindcss/postcss` from 4.3.0 to 4.3.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/@tailwindcss-postcss)

Updates `lucide-react` from 1.17.0 to 1.18.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.18.0/packages/lucide-react)

Updates `react-hook-form` from 7.77.0 to 7.78.0
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.77.0...v7.78.0)

Updates `@types/node` from 25.9.2 to 25.9.3
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `eslint` from 10.4.1 to 10.5.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.4.1...v10.5.0)

Updates `swagger-typescript-api` from 13.12.1 to 13.12.2
- [Release notes](https://github.com/acacode/swagger-typescript-api/releases)
- [Changelog](https://github.com/acacode/swagger-typescript-api/blob/main/CHANGELOG.md)
- [Commits](https://github.com/acacode/swagger-typescript-api/compare/v13.12.1...v13.12.2)

Updates `tailwindcss` from 4.3.0 to 4.3.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/tailwindcss)

Updates `typescript-eslint` from 8.60.1 to 8.61.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.61.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-alert-dialog"
  dependency-version: 1.1.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-avatar"
  dependency-version: 1.1.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-checkbox"
  dependency-version: 1.3.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-version: 2.1.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-navigation-menu"
  dependency-version: 1.2.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-popover"
  dependency-version: 1.1.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-scroll-area"
  dependency-version: 1.2.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-separator"
  dependency-version: 1.1.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-slot"
  dependency-version: 1.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-tabs"
  dependency-version: 1.1.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@tailwindcss/postcss"
  dependency-version: 4.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: lucide-react
  dependency-version: 1.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react-hook-form
  dependency-version: 7.78.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.9.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: eslint
  dependency-version: 10.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: swagger-typescript-api
  dependency-version: 13.12.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: tailwindcss
  dependency-version: 4.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.61.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-12 23:36:39 +00:00
Hosted WeblateandArif Budiman 47feb7fc68 Translated using Weblate (Indonesian)
Currently translated at 100.0% (301 of 301 strings)

Co-authored-by: Arif Budiman <arifpedia@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/id/
Translation: Nezha/Admin frontend
2026-06-06 02:01:06 +00:00
naiba 2ff923220f fix: migrate to tailwind v4 and TS6, add oauth2 dashboard_host setting
- migrate Tailwind v3->v4 (postcss @tailwindcss/postcss, CSS-first @theme/@plugin/@custom-variant)
- fix TS6 build: ignoreDeprecations for baseUrl, react-day-picker v10 classNames, test type fixes
- add dashboard_host setting field with i18n
2026-06-06 01:39:25 +00:00
dependabot[bot] ee99e93e5e chore(deps-dev): bump flatted from 3.3.3 to 3.4.2 (#165)
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.3 to 3.4.2.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-06 08:17:48 +08:00
dependabot[bot] 1589fe6a4c chore(deps): bump the github-actions group with 8 updates (#160)
Bumps the github-actions group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [actions/checkout](https://github.com/actions/checkout) | `4` | `6` |
| [oven-sh/setup-bun](https://github.com/oven-sh/setup-bun) | `1` | `2` |
| [stefanzweifel/git-auto-commit-action](https://github.com/stefanzweifel/git-auto-commit-action) | `5` | `7` |
| [actions/github-script](https://github.com/actions/github-script) | `7` | `9` |
| [actions/setup-node](https://github.com/actions/setup-node) | `4` | `6` |
| [actions/setup-go](https://github.com/actions/setup-go) | `5` | `6` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `4` | `7` |
| [softprops/action-gh-release](https://github.com/softprops/action-gh-release) | `2` | `3` |


Updates `actions/checkout` from 4 to 6
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

Updates `oven-sh/setup-bun` from 1 to 2
- [Release notes](https://github.com/oven-sh/setup-bun/releases)
- [Commits](https://github.com/oven-sh/setup-bun/compare/v1...v2)

Updates `stefanzweifel/git-auto-commit-action` from 5 to 7
- [Release notes](https://github.com/stefanzweifel/git-auto-commit-action/releases)
- [Changelog](https://github.com/stefanzweifel/git-auto-commit-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stefanzweifel/git-auto-commit-action/compare/v5...v7)

Updates `actions/github-script` from 7 to 9
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v7...v9)

Updates `actions/setup-node` from 4 to 6
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

Updates `actions/setup-go` from 5 to 6
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

Updates `actions/upload-artifact` from 4 to 7
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v7)

Updates `softprops/action-gh-release` from 2 to 3
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: oven-sh/setup-bun
  dependency-version: '2'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: stefanzweifel/git-auto-commit-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/github-script
  dependency-version: '9'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/setup-go
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: softprops/action-gh-release
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-06 08:15:40 +08:00
dependabot[bot] bad0c5d4b4 chore(deps): bump the npm-dependencies group with 44 updates (#164)
Bumps the npm-dependencies group with 44 updates:

| Package | From | To |
| --- | --- | --- |
| [@hookform/resolvers](https://github.com/react-hook-form/resolvers) | `5.2.2` | `5.4.0` |
| [@radix-ui/react-avatar](https://github.com/radix-ui/primitives) | `1.1.10` | `1.1.11` |
| [@radix-ui/react-label](https://github.com/radix-ui/primitives) | `2.1.7` | `2.1.8` |
| [@radix-ui/react-separator](https://github.com/radix-ui/primitives) | `1.1.7` | `1.1.8` |
| [@radix-ui/react-slot](https://github.com/radix-ui/primitives) | `1.2.3` | `1.2.4` |
| [@tailwindcss/postcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss) | `4.1.14` | `4.3.0` |
| [@trivago/prettier-plugin-sort-imports](https://github.com/trivago/prettier-plugin-sort-imports) | `5.2.2` | `6.0.2` |
| [@xterm/addon-attach](https://github.com/xtermjs/xterm.js) | `0.11.0` | `0.12.0` |
| [@xterm/addon-fit](https://github.com/xtermjs/xterm.js) | `0.10.0` | `0.11.0` |
| [@xterm/xterm](https://github.com/xtermjs/xterm.js) | `5.5.0` | `6.0.0` |
| [copy-to-clipboard](https://github.com/sudodoki/copy-to-clipboard) | `3.3.3` | `4.0.2` |
| [date-fns](https://github.com/date-fns/date-fns) | `4.1.0` | `4.4.0` |
| [framer-motion](https://github.com/motiondivision/motion) | `12.23.22` | `12.40.0` |
| [i18next](https://github.com/i18next/i18next) | `25.5.3` | `26.3.1` |
| [i18next-browser-languagedetector](https://github.com/i18next/i18next-browser-languageDetector) | `8.2.0` | `8.2.1` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.545.0` | `1.17.0` |
| [prettier-plugin-tailwindcss](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) | `0.6.14` | `0.8.0` |
| [react](https://github.com/facebook/react/tree/HEAD/packages/react) | `19.2.0` | `19.2.7` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.14` | `19.2.17` |
| [react-day-picker](https://github.com/gpbl/react-day-picker/tree/HEAD/packages/react-day-picker) | `9.13.2` | `10.0.1` |
| [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) | `19.2.0` | `19.2.7` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.71.1` | `7.77.0` |
| [react-i18next](https://github.com/i18next/react-i18next) | `16.0.0` | `17.0.8` |
| [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `7.13.0` | `7.17.0` |
| [react-virtuoso](https://github.com/petyosi/react-virtuoso/tree/HEAD/packages/react-virtuoso) | `4.14.1` | `4.18.7` |
| [swr](https://github.com/vercel/swr) | `2.3.6` | `2.4.1` |
| [tailwind-merge](https://github.com/dcastil/tailwind-merge) | `3.3.1` | `3.6.0` |
| [zod](https://github.com/colinhacks/zod) | `4.3.6` | `4.4.3` |
| [zustand](https://github.com/pmndrs/zustand) | `5.0.8` | `5.0.14` |
| [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) | `9.39.2` | `10.0.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `24.10.13` | `25.9.2` |
| [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `5.0.4` | `6.0.2` |
| [autoprefixer](https://github.com/postcss/autoprefixer) | `10.4.21` | `10.5.0` |
| [eslint](https://github.com/eslint/eslint) | `9.39.2` | `10.4.1` |
| [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) | `7.0.1` | `7.1.1` |
| [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) | `0.4.23` | `0.5.2` |
| [globals](https://github.com/sindresorhus/globals) | `16.4.0` | `17.6.0` |
| [postcss](https://github.com/postcss/postcss) | `8.4.24` | `8.5.15` |
| [swagger-typescript-api](https://github.com/acacode/swagger-typescript-api) | `13.2.17` | `13.12.1` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `3.4.19` | `4.3.0` |
| [typescript](https://github.com/microsoft/TypeScript) | `5.9.3` | `6.0.3` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.55.0` | `8.60.1` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.3.1` | `8.0.16` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.7` | `4.1.8` |


Updates `@hookform/resolvers` from 5.2.2 to 5.4.0
- [Release notes](https://github.com/react-hook-form/resolvers/releases)
- [Commits](https://github.com/react-hook-form/resolvers/compare/v5.2.2...v5.4.0)

Updates `@radix-ui/react-avatar` from 1.1.10 to 1.1.11
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-label` from 2.1.7 to 2.1.8
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-separator` from 1.1.7 to 1.1.8
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-slot` from 1.2.3 to 1.2.4
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@tailwindcss/postcss` from 4.1.14 to 4.3.0
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.0/packages/@tailwindcss-postcss)

Updates `@trivago/prettier-plugin-sort-imports` from 5.2.2 to 6.0.2
- [Release notes](https://github.com/trivago/prettier-plugin-sort-imports/releases)
- [Changelog](https://github.com/trivago/prettier-plugin-sort-imports/blob/main/CHANGELOG.md)
- [Commits](https://github.com/trivago/prettier-plugin-sort-imports/compare/v5.2.2...v6.0.2)

Updates `@xterm/addon-attach` from 0.11.0 to 0.12.0
- [Release notes](https://github.com/xtermjs/xterm.js/releases)
- [Commits](https://github.com/xtermjs/xterm.js/compare/0.11...0.12)

Updates `@xterm/addon-fit` from 0.10.0 to 0.11.0
- [Release notes](https://github.com/xtermjs/xterm.js/releases)
- [Commits](https://github.com/xtermjs/xterm.js/compare/0.10...0.11)

Updates `@xterm/xterm` from 5.5.0 to 6.0.0
- [Release notes](https://github.com/xtermjs/xterm.js/releases)
- [Commits](https://github.com/xtermjs/xterm.js/compare/5.5.0...6.0.0)

Updates `copy-to-clipboard` from 3.3.3 to 4.0.2
- [Release notes](https://github.com/sudodoki/copy-to-clipboard/releases)
- [Changelog](https://github.com/sudodoki/copy-to-clipboard/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sudodoki/copy-to-clipboard/compare/v3.3.3...v4.0.2)

Updates `date-fns` from 4.1.0 to 4.4.0
- [Release notes](https://github.com/date-fns/date-fns/releases)
- [Commits](https://github.com/date-fns/date-fns/compare/v4.1.0...v4.4.0)

Updates `framer-motion` from 12.23.22 to 12.40.0
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.23.22...v12.40.0)

Updates `i18next` from 25.5.3 to 26.3.1
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v25.5.3...v26.3.1)

Updates `i18next-browser-languagedetector` from 8.2.0 to 8.2.1
- [Changelog](https://github.com/i18next/i18next-browser-languageDetector/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next-browser-languageDetector/compare/v8.2.0...v8.2.1)

Updates `lucide-react` from 0.545.0 to 1.17.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.17.0/packages/lucide-react)

Updates `prettier-plugin-tailwindcss` from 0.6.14 to 0.8.0
- [Release notes](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.14...v0.8.0)

Updates `react` from 19.2.0 to 19.2.7
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.7/packages/react)

Updates `@types/react` from 19.2.14 to 19.2.17
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `react-day-picker` from 9.13.2 to 10.0.1
- [Release notes](https://github.com/gpbl/react-day-picker/releases)
- [Changelog](https://github.com/gpbl/react-day-picker/blob/main/packages/react-day-picker/CHANGELOG.md)
- [Commits](https://github.com/gpbl/react-day-picker/commits/v10.0.1/packages/react-day-picker)

Updates `react-dom` from 19.2.0 to 19.2.7
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.7/packages/react-dom)

Updates `react-hook-form` from 7.71.1 to 7.77.0
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.71.1...v7.77.0)

Updates `react-i18next` from 16.0.0 to 17.0.8
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v16.0.0...v17.0.8)

Updates `react-router-dom` from 7.13.0 to 7.17.0
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.17.0/packages/react-router-dom)

Updates `react-virtuoso` from 4.14.1 to 4.18.7
- [Release notes](https://github.com/petyosi/react-virtuoso/releases)
- [Changelog](https://github.com/petyosi/react-virtuoso/blob/main/packages/react-virtuoso/CHANGELOG.md)
- [Commits](https://github.com/petyosi/react-virtuoso/commits/react-virtuoso@4.18.7/packages/react-virtuoso)

Updates `swr` from 2.3.6 to 2.4.1
- [Release notes](https://github.com/vercel/swr/releases)
- [Commits](https://github.com/vercel/swr/compare/v2.3.6...v2.4.1)

Updates `tailwind-merge` from 3.3.1 to 3.6.0
- [Release notes](https://github.com/dcastil/tailwind-merge/releases)
- [Commits](https://github.com/dcastil/tailwind-merge/compare/v3.3.1...v3.6.0)

Updates `zod` from 4.3.6 to 4.4.3
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v4.3.6...v4.4.3)

Updates `zustand` from 5.0.8 to 5.0.14
- [Release notes](https://github.com/pmndrs/zustand/releases)
- [Commits](https://github.com/pmndrs/zustand/compare/v5.0.8...v5.0.14)

Updates `@eslint/js` from 9.39.2 to 10.0.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v10.0.1/packages/js)

Updates `@types/node` from 24.10.13 to 25.9.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@types/react` from 19.2.14 to 19.2.17
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `@vitejs/plugin-react` from 5.0.4 to 6.0.2
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.2/packages/plugin-react)

Updates `autoprefixer` from 10.4.21 to 10.5.0
- [Release notes](https://github.com/postcss/autoprefixer/releases)
- [Changelog](https://github.com/postcss/autoprefixer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/autoprefixer/compare/10.4.21...10.5.0)

Updates `eslint` from 9.39.2 to 10.4.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.2...v10.4.1)

Updates `eslint-plugin-react-hooks` from 7.0.1 to 7.1.1
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/eslint-plugin-react-hooks@7.1.1/packages/eslint-plugin-react-hooks)

Updates `eslint-plugin-react-refresh` from 0.4.23 to 0.5.2
- [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases)
- [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.4.23...v0.5.2)

Updates `globals` from 16.4.0 to 17.6.0
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v16.4.0...v17.6.0)

Updates `postcss` from 8.4.24 to 8.5.15
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.24...8.5.15)

Updates `swagger-typescript-api` from 13.2.17 to 13.12.1
- [Release notes](https://github.com/acacode/swagger-typescript-api/releases)
- [Changelog](https://github.com/acacode/swagger-typescript-api/blob/main/CHANGELOG.md)
- [Commits](https://github.com/acacode/swagger-typescript-api/compare/13.2.17...v13.12.1)

Updates `tailwindcss` from 3.4.19 to 4.3.0
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.0/packages/tailwindcss)

Updates `typescript` from 5.9.3 to 6.0.3
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v6.0.3)

Updates `typescript-eslint` from 8.55.0 to 8.60.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.60.1/packages/typescript-eslint)

Updates `vite` from 7.3.1 to 8.0.16
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.16/packages/vite)

Updates `vitest` from 4.1.7 to 4.1.8
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.8/packages/vitest)

---
updated-dependencies:
- dependency-name: "@hookform/resolvers"
  dependency-version: 5.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-avatar"
  dependency-version: 1.1.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-separator"
  dependency-version: 1.1.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-slot"
  dependency-version: 1.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@tailwindcss/postcss"
  dependency-version: 4.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@trivago/prettier-plugin-sort-imports"
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: "@xterm/addon-attach"
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@xterm/addon-fit"
  dependency-version: 0.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@xterm/xterm"
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: copy-to-clipboard
  dependency-version: 4.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: date-fns
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: framer-motion
  dependency-version: 12.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: i18next
  dependency-version: 26.3.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: i18next-browser-languagedetector
  dependency-version: 8.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: lucide-react
  dependency-version: 1.17.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: prettier-plugin-tailwindcss
  dependency-version: 0.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react
  dependency-version: 19.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@types/react"
  dependency-version: 19.2.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: react-day-picker
  dependency-version: 10.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: react-dom
  dependency-version: 19.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: react-hook-form
  dependency-version: 7.77.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react-i18next
  dependency-version: 17.0.8
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: react-router-dom
  dependency-version: 7.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react-virtuoso
  dependency-version: 4.18.7
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: swr
  dependency-version: 2.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: tailwind-merge
  dependency-version: 3.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: zod
  dependency-version: 4.4.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: zustand
  dependency-version: 5.0.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@eslint/js"
  dependency-version: 10.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.9.2
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: "@types/react"
  dependency-version: 19.2.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: autoprefixer
  dependency-version: 10.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: eslint
  dependency-version: 10.4.1
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: eslint-plugin-react-hooks
  dependency-version: 7.1.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: eslint-plugin-react-refresh
  dependency-version: 0.5.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: globals
  dependency-version: 17.6.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: postcss
  dependency-version: 8.5.15
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: swagger-typescript-api
  dependency-version: 13.12.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: tailwindcss
  dependency-version: 4.3.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: typescript
  dependency-version: 6.0.3
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.60.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: vite
  dependency-version: 8.0.16
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: vitest
  dependency-version: 4.1.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-06 08:15:10 +08:00
naiba c706ffbd36 ci: add dependabot for npm and github actions 2026-06-05 22:53:28 +00:00
Nezha Dev b2d63355b9 feat(i18n): register ja, ro, uk, gl in language switcher
These four locales had translation files but were never wired into i18n
resources or the nezhaLang dropdown, so users could not select them.
Register ja-JP, ro-RO, uk-UA, gl-ES; missing keys fall back to en-US per
the Weblate community-translation workflow.
2026-06-05 01:13:16 +00:00
naiba aa0f6a201a feat(i18n): register ja, ro, uk, gl in language switcher
These four locales had translation files but were never wired into i18n
resources or the nezhaLang dropdown, so users could not select them.
Register ja-JP, ro-RO, uk-UA, gl-ES; missing keys fall back to en-US per
the Weblate community-translation workflow.
2026-06-05 01:13:16 +00:00
naiba dea01c1600 Merge pull request #159 from weblate/weblate-nezha-admin-frontend
Translations update from Hosted Weblate
2026-06-05 00:12:32 +08:00
naiba 353151b940 Merge pull request #159 from weblate/weblate-nezha-admin-frontend
Translations update from Hosted Weblate
2026-06-05 00:12:32 +08:00
071a436207 Translated using Weblate (Romanian)
Currently translated at 97.0% (293 of 302 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.6% (301 of 302 strings)

Translated using Weblate (Romanian)

Currently translated at 50.9% (154 of 302 strings)

Added translation using Weblate (Romanian)

Co-authored-by: A <cdparanoia@users.noreply.hosted.weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: UUBulb <uub@suwako.de>
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/ro/
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/zh_Hans/
Translation: Nezha/Admin frontend
2026-06-02 18:05:05 +02:00
9ae8baf7f3 Translated using Weblate (Romanian)
Currently translated at 97.0% (293 of 302 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.6% (301 of 302 strings)

Translated using Weblate (Romanian)

Currently translated at 50.9% (154 of 302 strings)

Added translation using Weblate (Romanian)

Co-authored-by: A <cdparanoia@users.noreply.hosted.weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: UUBulb <uub@suwako.de>
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/ro/
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/zh_Hans/
Translation: Nezha/Admin frontend
2026-06-02 18:05:05 +02:00
Nezha Dev 90135b0819 feat(service): replace show-in-service with hide-for-guest toggle
Mirror the server HideForGuest flag on services: rename the
enable_show_in_service field to hide_for_guest across the form, schema
and generated types, reuse the existing HideForGuest label, and drop the
now-orphaned ShowInService i18n keys. Services are now visible to guests
by default and hidden only when the box is checked, matching servers.
2026-06-02 03:07:36 +00:00
naiba 005b4a0b3b feat(service): replace show-in-service with hide-for-guest toggle
Mirror the server HideForGuest flag on services: rename the
enable_show_in_service field to hide_for_guest across the form, schema
and generated types, reuse the existing HideForGuest label, and drop the
now-orphaned ShowInService i18n keys. Services are now visible to guests
by default and hidden only when the box is checked, matching servers.
2026-06-02 03:07:36 +00:00
Nezha Dev 4c0de60843 feat(api-tokens): split inventory scope out of server scope
Surface the new nezha:inventory:{read,delete,*} scopes that gate listing
and deleting servers/server-groups, separate from nezha:server:* which now
covers per-server runtime operations. Refine server-scope descriptions to
match the backend split.
2026-05-31 15:10:03 +00:00
naiba 66e20c3860 feat(api-tokens): split inventory scope out of server scope
Surface the new nezha:inventory:{read,delete,*} scopes that gate listing
and deleting servers/server-groups, separate from nezha:server:* which now
covers per-server runtime operations. Refine server-scope descriptions to
match the backend split.
2026-05-31 15:10:03 +00:00
naibaandcloudcode bba5a8c44a test(e2e): fix revoke empty-list crash and password-restore CSRF cascade
Two CI-only failures surfaced against a fresh backend DB:

- The revoke test read after.data.find(), but the list endpoint omits data
  entirely when the admin has zero tokens, throwing on undefined. Default to [].
- The password-change test's restore POST hit a 403: changing the password
  triggers a refresh-token that re-mints the nz-csrf cookie, so the X-CSRF-Token
  read just before the request can be stale. A failed restore left the admin on
  the rotated password and cascaded into cron/fm/visibility login failures.
  Add csrfRequest(), which retries once on 403 after re-reading the cookie, and
  use it for both profile mutations.

Verified 8/8 passing across repeated fresh-DB CI-mode runs.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-31 08:45:19 +00:00
naibaandcloudcode 7057aa3098 test(e2e): fix revoke empty-list crash and password-restore CSRF cascade
Two CI-only failures surfaced against a fresh backend DB:

- The revoke test read after.data.find(), but the list endpoint omits data
  entirely when the admin has zero tokens, throwing on undefined. Default to [].
- The password-change test's restore POST hit a 403: changing the password
  triggers a refresh-token that re-mints the nz-csrf cookie, so the X-CSRF-Token
  read just before the request can be stale. A failed restore left the admin on
  the rotated password and cascaded into cron/fm/visibility login failures.
  Add csrfRequest(), which retries once on 403 after re-reading the cookie, and
  use it for both profile mutations.

Verified 8/8 passing across repeated fresh-DB CI-mode runs.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-31 08:45:19 +00:00
naibaandcloudcode 76b9eb6a54 test(e2e): send CSRF token on mutating requests and fix PAT UI selectors
The backend CSRF double-submit gate rejects unsafe methods unless
X-CSRF-Token mirrors the signed nz-csrf cookie. page.request bypasses the
SPA JS that does this, so every mutating E2E call got 403, failing the suite.

- Add csrfHeaders(page) helper that mirrors the nz-csrf cookie into the
  header, polling until the cookie is readable to avoid the post-login race.
- Apply it to all cookie-authenticated POST/PATCH/DELETE calls (the /mcp
  Bearer calls stay header-free since PAT requests are CSRF-exempt).
- loginAs waits for the nz-csrf cookie before returning.
- Fix the create-token dialog submit selector: the button is labelled
  'Create API token' (t('CreateApiToken')), not 'Create'.

Verified 8/8 passing across repeated CI-mode runs against a real backend.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-31 08:17:15 +00:00
naibaandcloudcode 22ebc51a08 test(e2e): send CSRF token on mutating requests and fix PAT UI selectors
The backend CSRF double-submit gate rejects unsafe methods unless
X-CSRF-Token mirrors the signed nz-csrf cookie. page.request bypasses the
SPA JS that does this, so every mutating E2E call got 403, failing the suite.

- Add csrfHeaders(page) helper that mirrors the nz-csrf cookie into the
  header, polling until the cookie is readable to avoid the post-login race.
- Apply it to all cookie-authenticated POST/PATCH/DELETE calls (the /mcp
  Bearer calls stay header-free since PAT requests are CSRF-exempt).
- loginAs waits for the nz-csrf cookie before returning.
- Fix the create-token dialog submit selector: the button is labelled
  'Create API token' (t('CreateApiToken')), not 'Create'.

Verified 8/8 passing across repeated CI-mode runs against a real backend.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-31 08:17:15 +00:00
naibaandcloudcode c9bed85a91 fix(security): guard external hrefs and refine CSRF/auth-loading handling
- Add safeExternalHref(): only render http(s) URLs as clickable hrefs,
  blocking attacker-controlled template metadata from becoming
  javascript:/data: links.
- Refine CSRF header attachment and auth-loading state in the fetcher,
  api-tokens client, useAuth, and settings route.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-31 05:51:39 +00:00
naibaandcloudcode 013770bf46 fix(security): guard external hrefs and refine CSRF/auth-loading handling
- Add safeExternalHref(): only render http(s) URLs as clickable hrefs,
  blocking attacker-controlled template metadata from becoming
  javascript:/data: links.
- Refine CSRF header attachment and auth-loading state in the fetcher,
  api-tokens client, useAuth, and settings route.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-31 05:51:39 +00:00
naibaandcloudcode 472174640a feat(settings): add reserved hosts field and surface NAT errors
Add the reserved_hosts setting (input + i18n across all locales) so operators
behind a reverse proxy can declare public dashboard hostnames members must not
register as NAT domains (GHSA-x6fg-52vr-hj4w).

Show the backend error message on NAT create/update failure instead of a
generic "unexpected error", so a reserved-host rejection is explained.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-31 02:04:47 +00:00
naibaandcloudcode 964684f1bf feat(settings): add reserved hosts field and surface NAT errors
Add the reserved_hosts setting (input + i18n across all locales) so operators
behind a reverse proxy can declare public dashboard hostnames members must not
register as NAT domains (GHSA-x6fg-52vr-hj4w).

Show the backend error message on NAT create/update failure instead of a
generic "unexpected error", so a reserved-host rejection is explained.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-31 02:04:47 +00:00
naibaandcloudcode 6f0d9b6637 feat(api-tokens): add PAT management UI, CSRF handling, and auth-loading fixes
Add an API tokens management route to create, list, and revoke PATs, showing
the plaintext token once on creation with scope and server-id selection.

Mirror the nz-csrf cookie into the X-CSRF-Token header on unsafe fetcher
methods (POST/PUT/PATCH/DELETE) for the server-side double-submit check, and
self-heal expired sessions via refresh-token without a recursive fetch loop.

Gate protected routes behind resolved auth state to avoid pre-auth SWR fetches,
and fix the login loading/race so stale probes cannot clobber the session.

Add i18n keys for the new screens across all locales.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-30 15:56:53 +00:00
naibaandcloudcode f07c557029 feat(api-tokens): add PAT management UI, CSRF handling, and auth-loading fixes
Add an API tokens management route to create, list, and revoke PATs, showing
the plaintext token once on creation with scope and server-id selection.

Mirror the nz-csrf cookie into the X-CSRF-Token header on unsafe fetcher
methods (POST/PUT/PATCH/DELETE) for the server-side double-submit check, and
self-heal expired sessions via refresh-token without a recursive fetch loop.

Gate protected routes behind resolved auth state to avoid pre-auth SWR fetches,
and fix the login loading/race so stale probes cannot clobber the session.

Add i18n keys for the new screens across all locales.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-30 15:56:53 +00:00
1f1df8e273 ci(e2e): bind Vite dev server to loopback
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 05:56:12 +00:00
f47d4a8772 ci(e2e): bind Vite dev server to loopback
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 05:56:12 +00:00
3db5041c58 ci(e2e): prebuild dashboard before backend health check
The GitHub runner can spend more than the readiness window compiling go run ./cmd/dashboard, leaving dashboard.log empty while the backend has not started yet. Build the dashboard binary during preparation and health-check only the actual server startup. Keep README instructions aligned with CI.

Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 05:36:09 +00:00
f380a8c4ac ci(e2e): prebuild dashboard before backend health check
The GitHub runner can spend more than the readiness window compiling go run ./cmd/dashboard, leaving dashboard.log empty while the backend has not started yet. Build the dashboard binary during preparation and health-check only the actual server startup. Keep README instructions aligned with CI.

Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 05:36:09 +00:00
11e2f2ca80 ci(e2e): make backend stubs embeddable and keep vitest scoped
The dashboard embed pattern ignores hidden-only dist directories, so the CI backend stub must create visible index.html files. Keep the README in sync and exclude Playwright specs from Vitest so npm run test remains a unit-test command.

Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 05:27:46 +00:00
e961fdb131 ci(e2e): make backend stubs embeddable and keep vitest scoped
The dashboard embed pattern ignores hidden-only dist directories, so the CI backend stub must create visible index.html files. Keep the README in sync and exclude Playwright specs from Vitest so npm run test remains a unit-test command.

Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 05:27:46 +00:00
naibaandcloudcode 84fac6d653 docs(e2e): describe how to run Playwright suite locally and read CI artifacts
Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 04:36:03 +00:00
naibaandcloudcode baec84bdc2 docs(e2e): describe how to run Playwright suite locally and read CI artifacts
Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 04:36:03 +00:00
naibaandcloudcode bb6abeb9a6 ci: run Playwright e2e against a real Go backend in GitHub Actions
Boot a fresh dashboard from nezhahq/nezha master inside the runner:

- Checkout admin-frontend and the backend repo into two paths.
- Generate the two embed stubs and swag docs so cmd/dashboard builds.
- Inject NZ_JWTSECRETKEY (env-first config story) and start the
  backend on 127.0.0.1:8008 with a tmp SQLite db.
- Wait on /api/v1/setting before letting Playwright launch.
- Playwright config brings up Vite dev which proxies /api to 8008.
- Upload playwright-report and dashboard.log on failure for triage.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 04:34:43 +00:00
naibaandcloudcode 52f67d22ba ci: run Playwright e2e against a real Go backend in GitHub Actions
Boot a fresh dashboard from nezhahq/nezha master inside the runner:

- Checkout admin-frontend and the backend repo into two paths.
- Generate the two embed stubs and swag docs so cmd/dashboard builds.
- Inject NZ_JWTSECRETKEY (env-first config story) and start the
  backend on 127.0.0.1:8008 with a tmp SQLite db.
- Wait on /api/v1/setting before letting Playwright launch.
- Playwright config brings up Vite dev which proxies /api to 8008.
- Upload playwright-report and dashboard.log on failure for triage.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 04:34:43 +00:00
naibaandcloudcode 22da4d74b8 test(e2e): add Playwright suite for auth + CSRF + visibility fixes
Covers the security fixes that landed across both repos:

- auth.spec.ts: login persists nz-jwt cookie and getProfile succeeds;
  password change bumps TokenVersion + revokes the old cookie so the
  pre-change JWT can no longer auth (regression guard for the
  keyId+session backend rewrite).
- cron-csrf.spec.ts: POST /api/v1/cron/:id/manual succeeds while GET
  is no longer routable (regression guard for the cron CSRF fix).
- fm-csrf.spec.ts: POST /api/v1/file is reachable while GET is no
  longer routable (regression guard for the FM CSRF fix).
- visibility.spec.ts: an anonymous caller cannot see a server-group
  that contains zero guest-visible servers (regression guard for the
  server-group leak fix).

Fixtures wrap the noisy login + cleanup boilerplate. tsconfig is
scoped to tests/e2e so the suite stays out of the production tsc
project graph.

Playwright config starts the Vite dev server (npm run dev) and
expects a backend reachable at the URL Vite proxies to. CI workflow
follow-up commit wires the backend up.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 04:34:43 +00:00
naibaandcloudcode dc0cfd0ecb test(e2e): add Playwright suite for auth + CSRF + visibility fixes
Covers the security fixes that landed across both repos:

- auth.spec.ts: login persists nz-jwt cookie and getProfile succeeds;
  password change bumps TokenVersion + revokes the old cookie so the
  pre-change JWT can no longer auth (regression guard for the
  keyId+session backend rewrite).
- cron-csrf.spec.ts: POST /api/v1/cron/:id/manual succeeds while GET
  is no longer routable (regression guard for the cron CSRF fix).
- fm-csrf.spec.ts: POST /api/v1/file is reachable while GET is no
  longer routable (regression guard for the FM CSRF fix).
- visibility.spec.ts: an anonymous caller cannot see a server-group
  that contains zero guest-visible servers (regression guard for the
  server-group leak fix).

Fixtures wrap the noisy login + cleanup boilerplate. tsconfig is
scoped to tests/e2e so the suite stays out of the production tsc
project graph.

Playwright config starts the Vite dev server (npm run dev) and
expects a backend reachable at the URL Vite proxies to. CI workflow
follow-up commit wires the backend up.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 04:34:43 +00:00
naibaandcloudcode 266cd7d150 fix(fm): switch createFM to POST to match backend CSRF fix
Backend moves /api/v1/file from GET to POST so SameSite=Lax cookies
can no longer expose this state-changing endpoint to cross-site
CSRF. Update createFM() accordingly.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 04:07:52 +00:00
naibaandcloudcode c80899a753 fix(fm): switch createFM to POST to match backend CSRF fix
Backend moves /api/v1/file from GET to POST so SameSite=Lax cookies
can no longer expose this state-changing endpoint to cross-site
CSRF. Update createFM() accordingly.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 04:07:52 +00:00
naibaandcloudcode b10d655d6e fix(cron): switch manual trigger to POST to match backend CSRF fix
Backend GHSA-8qhj-4f8c-j8qg moves /api/v1/cron/:id/manual from GET to
POST so SameSite=Lax cookies no longer expose this state-changing
endpoint to cross-site CSRF. Update runCron() accordingly.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 04:00:15 +00:00
naibaandcloudcode f2fea38e10 fix(cron): switch manual trigger to POST to match backend CSRF fix
Backend GHSA-8qhj-4f8c-j8qg moves /api/v1/cron/:id/manual from GET to
POST so SameSite=Lax cookies no longer expose this state-changing
endpoint to cross-site CSRF. Update runCron() accordingly.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 04:00:15 +00:00
naiba 849d131382 feat: server transfer rotation 2026-05-25 10:15:24 +00:00
naiba b23a6c0be1 feat: server transfer rotation 2026-05-25 10:15:24 +00:00
naibaandnaiba/CloudCode 2010d80914 fix: hide admin settings tabs for members
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-16 00:46:15 +08:00
naibaandnaiba/CloudCode 142fc19469 fix: hide admin settings tabs for members
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-16 00:46:15 +08:00
Weblate (bot)andArif Budiman 3912315f56 Translated using Weblate (Indonesian) (#156)
Currently translated at 100.0% (245 of 245 strings)


Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/id/
Translation: Nezha/Admin frontend

Co-authored-by: Arif Budiman <arifpedia@gmail.com>
2026-04-05 09:58:43 +08:00
naibaandnaiba/CloudCode b77ac7af93 fix: use Combobox for notification group selection in settings page
Settings page used a plain number input for ip_change_notification_group_id,
requiring users to manually enter a group ID. Replace it with a searchable
Combobox component (consistent with alert-rule/service/cron pages) and wrap
the settings route with NotificationProvider to load notification group data.

Closes nezhahq/nezha#1174

Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-03-01 01:11:37 +00:00
Weblate (bot)andРуслан Пузич d276c6843a Translated using Weblate (Ukrainian) (#155)
Currently translated at 49.7% (122 of 245 strings)


Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/uk/
Translation: Nezha/Admin frontend

Co-authored-by: Руслан Пузич <visp80@gmail.com>
2026-02-22 21:45:21 +08:00
Weblate (bot)andnlimeres 56b00f19ce Translated using Weblate (Galician) (#153)
Currently translated at 7.7% (19 of 245 strings)

Added translation using Weblate (Galician)

Translated using Weblate (Spanish)

Currently translated at 100.0% (244 of 244 strings)



Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/es/
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/gl/
Translation: Nezha/Admin frontend

Co-authored-by: nlimeres <dynosaurioprogramador@gmail.com>
2026-02-19 14:34:50 +08:00
奶爸andnaiba 6ddcd0eaea fix: public_note validation rejects non-object JSON values, preventing second edit (#154)
Co-authored-by: naiba <naiba@users.noreply.github.com>
2026-02-19 14:30:10 +08:00
UUBulb 76663b8d91 fix: downgrade tailwindcss to v3 (#151)
* fix: downgrade tailwindcss to v3

* chore: auto-fix linting and formatting issues
2026-02-14 18:06:12 +08:00
naiba 1b9c82ca5a fix: server edit dialog submit button unresponsive after first save (#1132)
Upgrade react-hook-form 7.63→7.71 to fix form.reset() race condition
that silently broke zod validation on subsequent submissions.

Reset form and public-note state (publicNoteObj/Raw/Mode/Errors)
from latest data when the edit dialog opens, preventing stale state
from a previous edit session.
2026-02-14 12:11:47 +08:00
胡说丷刂andhuYang 63338eee67 service 增加权重 (#150)
* 服务增加权重

* 避免显示成 undefined

---------

Co-authored-by: huYang <306061454@qq.com>
2026-02-14 10:10:24 +08:00
UUBulb 5031b8469b fix: variable not renamed (#148) 2026-01-10 20:22:13 +08:00
UUBulb f1e4ff8fb9 feat(notification): add option to convert metric units in request body (#147)
* feat(notification): add option to convert metric units in request body

* rename fields

* add prettierignore

* chore: auto-fix linting and formatting issues
2026-01-10 17:27:13 +08:00
naiba 13c78a8302 fix: immutable release 2026-01-01 11:58:11 +08:00
Weblate (bot)andРуслан Пузич a70953513b Translated using Weblate (Ukrainian) (#145)
Currently translated at 34.8% (85 of 244 strings)

Added translation using Weblate (Ukrainian)



Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/uk/
Translation: Nezha/Admin frontend

Co-authored-by: Руслан Пузич <visp80@gmail.com>
2026-01-01 10:32:17 +08:00
Weblate (bot)andZilong Liu 1b587f8ff7 Added translation using Weblate (Japanese) (#142)
Co-authored-by: Zilong Liu <2821624044@qq.com>
2025-10-26 21:32:10 +08:00
naiba 8ae5bee0c0 chore: fix import 2025-10-09 09:48:07 +08:00
91020bb15e feat: enhance public notes functionality with flexible options (#141)
* feat: enhance public notes functionality with flexible options

- Make public notes optional to prevent default values causing frontend issues
- Add dual editing modes: raw text editing and custom fields (avoid hardcoded schema)
- Set raw text mode as default, populate input on edit, submit raw text content always
- Add toggle switch: submit raw text when enabled, submit empty & hide controls when disabled
- New records default to disabled public notes; auto-expand on edit based on content

* chore: auto-fix linting and formatting issues

* feat: Add public annotation data structure and utility functions

Implemented Zod validation patterns, default values, parsing functions, and utility functions for public notes, and updated related internationalization text.

* chore: auto-fix linting and formatting issues

* refactor(server): Replace i18n implementation

Replace direct use of i18n.t with react-i18next's useTranslation hook to improve internationalization support.

* refactor(public-note): Optimize data model and validation logic

Removed the pruneEmpty function and simplified the date processing logic, making billingDataMod and planDataMod optional fields. Also optimized the validation logic to handle optional fields.

* chore: auto-fix linting and formatting issues

* fix zod validation & don't write empty values when parsing

* use raw mode if object contains unknown fields

* rename some features

* chore: Update dependency package versions

Upgrade multiple npm dependencies to their latest versions, including react, tailwindcss, and eslint. Ignore lock files.

* fix(server): Fix default value when bill amount is undefined

Changed undefined values ​​for bill amount to the default value "0" to avoid potential null value errors.

---------

Co-authored-by: Guccen <171530509+Chillln@users.noreply.github.com>
Co-authored-by: uubulb <uub@suwako.de>
2025-10-09 09:35:13 +08:00
naiba 4806ff35fe 💩 f*ck react 2025-10-02 17:15:20 +08:00
naiba 61186143bd fix: Cannot read properties of undefined (reading 'displayName') 2025-10-02 15:26:31 +08:00
ChilllnandGuccen c27a13a67b Fix and update (#139)
* refactor(ui): 统一组件引用类型为ComponentRef

更新所有UI组件中的forwardRef类型,从ElementRef改为ComponentRef以保持一致性
迁移postcss配置至mjs格式并更新依赖版本

* refactor: 优化表单类型定义和验证逻辑

移除自定义的 asOptionalField 工具函数,直接使用 Zod 的 optional() 方法,并明确定义表单数据类型。

* style: 更新UI主题配置和样式变量

将主题风格从default切换为new-york,并重构CSS变量使用OKLCH色彩空间。同时添加tailwindcss-animate插件支持。

* style: 统一页面头部按钮组样式

优化多个页面头部按钮组的布局样式,增加响应式设计和flex-wrap支持

* fix(server): 修复对话框交互问题并优化SWR配置

修复对话框关闭逻辑并阻止外部交互,同时禁用SWR的自动重新验证功能以提升性能。

* feat: 添加日历组件及账单相关国际化

实现基于 react-day-picker 的日历组件,并添加账单管理相关的多语言支持

* style(components): 统一按钮样式并格式化代码

为删除和禁用按钮添加text-white类名,同时调整ServerCard组件中的代码缩进格式。

* perf(build): 优化Vite打包配置与代码分割策略

调整Vite构建配置,改进第三方依赖的分组逻辑并添加UUID支持到安装命令组件

* fix: 修正页面标题翻译不一致问题

将CronPage和ServicePage的标题从"Server"分别改为"Task"和"Service",并优化NotificationGroupPage的按钮组布局。

* fix(auth): 改进登录错误处理和国际化支持

优化登录错误提示,添加多语言支持并移除控制台错误日志。同时修复头部组件透明度样式问题。

* feat: 添加服务器操作下拉菜单

为服务器卡片添加统一的下拉菜单操作入口,整合终端、配置和安装命令功能。

* feat[alert-rule]: 优化告警规则组件性能

重构告警规则组件代码结构,提升渲染效率并减少内存占用。

* docs(i18n): 新增翻译字段

为界面添加"Add"、"Delete"、"AdvancedJSON"和"Save"等关键操作的翻译字段,支持中英文双语显示。

* perf(vite): 优化分包策略以提升构建性能

重构 manualChunks 逻辑,按功能类别分组依赖项,并增加大型库的独立分包规则。

* style: 统一危险操作按钮的文字颜色

在所有确认操作的弹窗按钮中添加白色文字样式,保持视觉一致性。

* fix(components): 调整下拉菜单对齐方式

根据菜单项状态动态设置下拉菜单的对齐方向和起始位置。

* fix(types): 修复在线用户API分页类型

添加ModelOnlineUserApi接口类型,包含分页信息,并移除index.ts中重复的类型定义。

* chore: auto-fix linting and formatting issues

* feat(locales): 添加无过期相关翻译项

为英文和中文翻译文件添加"NoExpiry"、"SetNoExpiry"等无过期相关字段的翻译。

fix(components): 移除重复的图标按钮选项

从IconButton组件中删除重复的"more"图标选项。

* feat(ServerCard): 优化日期选择器并添加下拉提示

为日期选择器添加下拉布局和年份范围限制,并在公共笔记区域增加下拉项生效提示文本。

* chore: auto-fix linting and formatting issues

* style: 优化多个组件的UI交互细节

统一按钮悬停样式并简化国际化文本调用,移除冗余的单位显示和空值判断逻辑。

* refactor(ServerCard): 移除网络路由相关代码

删除 ServerCard 组件中与 plan.networkRoute 相关的字段验证和错误显示逻辑。

* chore: auto-fix linting and formatting issues

* feat(ui): 添加Switch组件并改进服务器表单交互

- 新增Radix UI Switch组件依赖及实现
- 将IPv4/IPv6输入改为开关控件,优化用户体验
- 添加"按量付费"选项和新的翻译字段
- 改进网络路由和备注输入的占位提示
- 修复暗黑模式下的按钮背景色

* style(components): 为禁止按钮添加白色文本样式

* chore: auto-fix linting and formatting issues

* fix(ServerCard): 修复日期选择器样式和滚动问题

调整日期选择器的宽度和高度限制,添加滚动容器以解决内容溢出问题

* refactor(server-config): 简化复选框checked属性的布尔转换

使用!!操作符简化controllerField.value的布尔值转换,使代码更简洁

* feat(国际化): 添加告警规则和搜索框的国际化支持

为告警规则组件添加多语言支持,包括服务器监控选项、忽略提示和示例文本。同时将搜索框的占位文本替换为国际化字段。

* chore: auto-fix linting and formatting issues

* fix(switch): 修正 Switch 组件 ref 类型定义错误

---------

Co-authored-by: Guccen <171530509+Chillln@users.noreply.github.com>
2025-10-02 14:23:11 +08:00
naiba 10a04a2daf chore: add locales 2025-08-23 21:09:54 +08:00
Weblate (bot)andTurboHK b6f62635e3 Translated using Weblate (Chinese (Traditional Han script)) (#137)
Currently translated at 100.0% (181 of 181 strings)


Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/zh_Hant/
Translation: Nezha/Admin frontend

Co-authored-by: TurboHK <turbohkcoltd@gmail.com>
2025-08-23 21:07:02 +08:00
Weblate (bot)andTommyLPB ee6079fa58 Translated using Weblate (French) (#136)
Currently translated at 48.6% (88 of 181 strings)


Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/fr/
Translation: Nezha/Admin frontend

Co-authored-by: TommyLPB <hcrochetcanard@gmail.com>
2025-08-02 15:53:17 +08:00
Weblate (bot)andTommyLPB d8d34abf2f Translated using Weblate (French) (#135)
Currently translated at 48.0% (87 of 181 strings)


Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/fr/
Translation: Nezha/Admin frontend

Co-authored-by: TommyLPB <hcrochetcanard@gmail.com>
2025-07-27 10:10:09 +08:00
e05fe015a3 Translated using Weblate (French) (#133)
Currently translated at 25.9% (47 of 181 strings)

Added translation using Weblate (French)

Translated using Weblate (Indonesian)

Currently translated at 9.9% (18 of 181 strings)




Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/fr/
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/id/
Translation: Nezha/Admin frontend

Co-authored-by: Kxroqi :3 <loxocker@gmail.com>
Co-authored-by: TommyLPB <hcrochetcanard@gmail.com>
2025-07-25 21:46:13 +08:00
naiba 01ed6eae54 feat: 批量转移服务器给其他用户 2025-06-16 23:44:30 +08:00
6fe192b944 Added translation using Weblate (Indonesian) (#132)
Translated using Weblate (Spanish)

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (180 of 180 strings)




Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/es/
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/zh_Hans/
Translation: Nezha/Admin frontend

Co-authored-by: Kris <krishoodbyair@gmail.com>
Co-authored-by: Kxroqi :3 <loxocker@gmail.com>
2025-06-08 00:49:08 +08:00
TomyJan 1f2a187d7b feat: separate real ip header of frontend/agent (#130)
* feat: separate real ip header of frontend/agent

* test(ci): test

* Revert "test(ci): test"

This reverts commit 7d2f16ed24e5f95f6101dc3ac67816ece2dc5ade.

* chore: auto-fix linting and formatting issues
2025-04-13 18:40:35 +08:00
naiba adac229ab4 chore(i18n): add locale config options 2025-03-29 13:21:14 +08:00
Weblate (bot)andAndres Pava 10683c88b7 Translated using Weblate (Spanish) (#128)
Currently translated at 96.1% (173 of 180 strings)


Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/es/
Translation: Nezha/Admin frontend

Co-authored-by: Andres Pava <hola@andrespava.com>
2025-03-29 11:27:36 +08:00
naiba 223cb9c1ae feat(i18n): add locales 2025-03-27 19:11:56 +08:00
UUBulb 5e749180d5 fix config pusher (#127)
* fix config pusher

* chore: auto-fix linting and formatting issues
2025-03-25 22:40:56 +08:00
naiba 66945cdb15 chore: update locale 2025-03-22 22:37:01 +08:00
Weblate (bot)andbios let f75553d1cb Translated using Weblate (Russian) (#126)
Currently translated at 100.0% (180 of 180 strings)

Added translation using Weblate (Russian)



Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/ru/
Translation: Nezha/Admin frontend

Co-authored-by: bios let <bioslet@gmail.com>
2025-03-22 22:33:15 +08:00
Moraxyc 0cbd0df8ea fix(deps): upgrade next-themes to ^0.4.6 for React 19 compatibility (#125)
Upgraded `next-themes` from `^0.3.0` to `^0.4.6` to resolve compatibility issues with React 19.
The previous version (`0.3.0`) does not support React 19, which caused conflicts.

Changes:
- Updated `package.json` dependency: `"next-themes": "^0.4.6"`
- `bun.lockb` updated accordingly.
2025-03-16 12:09:14 +08:00
UUBulb 1ca8ea4627 update setting api (#123) 2025-03-08 18:42:55 +08:00
UUBulb a56238bed7 update settings api (#121)
* update settings api

* chore: auto-fix linting and formatting issues
2025-02-28 22:00:07 +08:00
UUBulb 51e34e30c3 chore: correct cron experssion placeholder (#120) 2025-02-20 22:57:08 +08:00
UUBulb 9ea2b3a11a fix ws protocol (#117) 2025-02-07 22:16:45 +08:00
UUBulb d96a2e4c3c bug fixes (#116)
* bug fixes

* fix ws url
2025-02-05 18:46:42 +08:00
UUBulb a52bf64779 feat: batch set server config (#114)
* feat: batch set server config

* make every field optional

* chore: auto-fix linting and formatting issues

* update

* [WIP] improve batch edit ux

* chore: auto-fix linting and formatting issues
2025-02-04 11:19:52 +08:00
Weblate (bot)andUUBulb 70b8f39bd6 Translated using Weblate (Chinese (Traditional Han script)) (#113)
Currently translated at 100.0% (177 of 177 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (177 of 177 strings)


Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/zh_Hant/
Translation: Nezha/Admin frontend

Co-authored-by: UUBulb <uub@kuzu.uk>
2025-02-04 11:14:58 +08:00
UUBulb a00bf5826a feat: edit server config online (#112)
* feat: edit server config online

* fix schema

* fix error
2025-01-31 13:32:16 +08:00
UUBulb 1b19f74935 ddns: allow overriding domains per configuration (#111) 2025-01-30 12:15:21 +08:00
UUBulb 769d10b8d7 minor bug fixes (#110) 2025-01-29 15:07:20 +08:00
naiba f6ce25d466 feat: disable nat 2025-01-21 22:21:52 +08:00
a6b6174bbf Translated using Weblate (German) (#107)
Currently translated at 100.0% (175 of 175 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 99.4% (174 of 175 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (175 of 175 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (175 of 175 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (175 of 175 strings)

Added translation using Weblate (Tamil)





Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/de/
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/ta/
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/zh_Hant/
Translation: Nezha/Admin frontend

Co-authored-by: Barium <peter.kuehfuss+weblate@gmail.com>
Co-authored-by: UUBulb <uub@kuzu.uk>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
2025-01-21 20:47:01 +08:00
UUBulb dd5b23eea7 fix: prevent null dereferences (#106) 2025-01-20 22:08:18 +08:00
7e20e3d215 Translated using Weblate (Chinese (Traditional Han script)) (#105)
Currently translated at 99.4% (174 of 175 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (175 of 175 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (175 of 175 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (175 of 175 strings)

Added translation using Weblate (Tamil)




Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/ta/
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/zh_Hant/
Translation: Nezha/Admin frontend

Co-authored-by: UUBulb <uub@kuzu.uk>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
2025-01-20 22:07:57 +08:00
UUBulb d49672bdbe chore: update translations (#101)
* chore: update translations

* fix: form values type conversion

* feat: terminal fullscreen mode

* fix
2025-01-19 21:09:52 +08:00
UUBulb 689a997a63 fix(fm): close dialog after upload completed (#97) 2025-01-07 19:54:47 +08:00
仓鼠 5f7553c75a fix: Fix login provider button gap (#94)
* fix: oauth2_providers button gap

* fix: remove test code
2025-01-06 21:02:58 +08:00
UUBulb c883aba1fc improve: replace long line of words with clipboard button (#95)
* improve: replace long line of words with clipboard button

* change module path
2025-01-06 21:02:39 +08:00
仓鼠 b88102b4aa feat: add back to home link on mobile (#93) 2025-01-05 22:56:19 +08:00
仓鼠 b6d185eba1 fix: back to home page redirect (#90) 2025-01-04 20:47:37 +08:00
UUBulb b6c5efc757 update waf api (#91) 2025-01-04 20:47:14 +08:00
仓鼠andhamster1963 9f6722edcd feat: menu add back to home item (#89)
* feat: menu add back to home item

* chore: auto-fix linting and formatting issues

---------

Co-authored-by: hamster1963 <hamster1963@users.noreply.github.com>
2025-01-04 18:42:58 +08:00
UUBulbanduubulb ab10ffb355 improve: add missing try-catch cases (#86)
* improve: add missing try-catch cases

* i18n: support german

* chore: auto-fix linting and formatting issues

---------

Co-authored-by: uubulb <uubulb@users.noreply.github.com>
2025-01-03 22:43:36 +08:00
仓鼠andhamster1963 c7d2260307 fix: footer year text (#88)
* fix: footer year text

* fix: oauth2 list add space

* chore: auto-fix linting and formatting issues

---------

Co-authored-by: hamster1963 <hamster1963@users.noreply.github.com>
2025-01-03 22:43:00 +08:00
Weblate (bot)andBarium d91c2ca3cc Translated using Weblate (German) (#87)
Currently translated at 100.0% (171 of 171 strings)



Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/de/
Translation: Nezha/Admin frontend

Co-authored-by: Barium <peter.kuehfuss+weblate@gmail.com>
2025-01-03 22:42:32 +08:00
Weblate (bot)andUUBulb 8dc03d22d6 Translated using Weblate (Chinese (Traditional Han script)) (#85)
Currently translated at 100.0% (171 of 171 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (171 of 171 strings)


Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/zh_Hant/
Translation: Nezha/Admin frontend

Co-authored-by: UUBulb <uub@kuzu.uk>
2024-12-31 23:59:39 +08:00
UUBulbanduubulb 5d8a0404b7 update oauth2 (#84)
* update oauth2

* i18n

* fix validation

* chore: auto-fix linting and formatting issues

---------

Co-authored-by: uubulb <uubulb@users.noreply.github.com>
2024-12-31 23:11:41 +08:00
naiba aa00431f8b chore: add type 2024-12-30 21:15:17 +08:00
仓鼠andhamster1963 5ffdefc8dd style: login button (#83)
* style: login button

* chore: auto-fix linting and formatting issues

---------

Co-authored-by: hamster1963 <hamster1963@users.noreply.github.com>
2024-12-30 20:45:11 +08:00
18c832d553 optimize fm (#82)
* optimize fm

* chore: auto-fix linting and formatting issues

* fix: type

* chore: auto-fix linting and formatting issues

---------

Co-authored-by: uubulb <uubulb@users.noreply.github.com>
Co-authored-by: naiba <hi@nai.ba>
Co-authored-by: naiba <naiba@users.noreply.github.com>
2024-12-29 22:19:50 +08:00
naiba 718bff0abb feat: oauth2 登录 2024-12-28 23:50:24 +08:00
f48d0a6393 Translated using Weblate (German) (#77)
Currently translated at 100.0% (170 of 170 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (170 of 170 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (170 of 170 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (170 of 170 strings)




Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/de/
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/es/
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/it/
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/zh_Hant/
Translation: Nezha/Admin frontend

Co-authored-by: Kris <krishoodbyair@gmail.com>
Co-authored-by: UUBulb <uub@kuzu.uk>
2024-12-26 22:31:51 +08:00
Weblate (bot)andKris 0813b9fd45 Translated using Weblate (Spanish) (#75)
Currently translated at 68.8% (117 of 170 strings)


Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/es/
Translation: Nezha/Admin frontend

Co-authored-by: Kris <krishoodbyair@gmail.com>
2024-12-25 20:24:59 +08:00
naiba 8130e336a9 fix: block button 2024-12-23 19:47:57 +08:00
naiba 86115547a5 fix: member role menu 2024-12-23 19:24:46 +08:00
仓鼠 298cc4ca2a fix: incorrect settings page navigate (#72) 2024-12-23 19:18:39 +08:00
仓鼠andhamster1963 f694c97704 fix: i18n (#71)
* fix: block_identifier text

* fix: i18n

* chore: auto-fix linting and formatting issues

* fix: block button

* fix: ConfirmBlock text

* fix: i18n

---------

Co-authored-by: hamster1963 <hamster1963@users.noreply.github.com>
2024-12-23 00:38:40 +08:00
仓鼠andhamster1963 7fdac38291 User Role (#69)
* fix: window.DisableAnimatedMan as boolean

* chore: auto-fix linting and formatting issues

* feat: user role

* feat: use user agent_secret

* feat: hide setting when user role is not admin

* feat: new waf api

* chore: auto-fix linting and formatting issues

* fix: admin settings page

* feat: online-user setting

* fix: pagination

---------

Co-authored-by: hamster1963 <hamster1963@users.noreply.github.com>
2024-12-22 23:17:20 +08:00
仓鼠andhamster1963 5ad915c973 fix: window.DisableAnimatedMan as boolean (#62)
* fix: window.DisableAnimatedMan as boolean

* chore: auto-fix linting and formatting issues

---------

Co-authored-by: hamster1963 <hamster1963@users.noreply.github.com>
2024-12-19 21:54:24 +08:00
90e8e4b620 Translated using Weblate (Italian) (#63)
Currently translated at 100.0% (160 of 160 strings)

Added translation using Weblate (Spanish)

Translated using Weblate (German)

Currently translated at 100.0% (160 of 160 strings)

Added translation using Weblate (German)





Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/de/
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/it/
Translation: Nezha/Admin frontend

Co-authored-by: Barium <peter.kuehfuss+weblate@gmail.com>
Co-authored-by: Kris <krishoodbyair@gmail.com>
Co-authored-by: UUBulb <uub@kuzu.uk>
2024-12-19 21:49:00 +08:00
naiba 76f868d242 fix: typo & render error 2024-12-17 22:19:22 +08:00
naiba a1ca16b297 feat: implement InjectContext for dynamic resource injection 2024-12-17 22:03:21 +08:00
仓鼠andhamster1963 6d9928abd3 fix: use system as defaultTheme (#60)
* fix: use system as defaultTheme

* chore: auto-fix linting and formatting issues

* fix: AnimatedMan position on login page

* fix: login form overflow on mobile

* fix: theme selected value display overflow

* fix: dropdown menu username overflow

---------

Co-authored-by: hamster1963 <hamster1963@users.noreply.github.com>
2024-12-17 21:51:16 +08:00
d0d33dbb65 Added translation using Weblate (Spanish) (#59)
Translated using Weblate (German)

Currently translated at 100.0% (160 of 160 strings)

Added translation using Weblate (German)




Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/de/
Translation: Nezha/Admin frontend

Co-authored-by: Barium <peter.kuehfuss+weblate@gmail.com>
Co-authored-by: Kris <krishoodbyair@gmail.com>
2024-12-17 21:47:09 +08:00
naiba 9204e0ae37 chore: refactor 2024-12-15 20:26:35 +08:00
UUBulbanduubulb 43a97b9175 fix install commands (#57)
* fix install commands

* allow disabling animated man

* chore: auto-fix linting and formatting issues

---------

Co-authored-by: uubulb <uubulb@users.noreply.github.com>
2024-12-15 19:38:35 +08:00
naiba 1a2c5a5a44 chore: change key 2024-12-15 15:27:23 +08:00
naiba 71e2f1a699 feat: 后台自定义代码 & 后端语言优先 2024-12-15 15:08:57 +08:00
naiba 5bd0e1c073 fix: animate man position 2024-12-15 10:57:47 +08:00
仓鼠andhamster1963 0314765765 style: header animated image (#54)
* style: header animated image

* chore: auto-fix linting and formatting issues

---------

Co-authored-by: hamster1963 <hamster1963@users.noreply.github.com>
2024-12-15 09:45:56 +08:00
UUBulb ee752b1ba7 fix: language code conversion (#53) 2024-12-14 17:17:53 +08:00
78afbb5609 Translations update from Hosted Weblate (#45)
* Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (158 of 158 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (158 of 158 strings)

Translated using Weblate (English)

Currently translated at 100.0% (158 of 158 strings)

Translated using Weblate (English)

Currently translated at 100.0% (158 of 158 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (158 of 158 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 96.8% (153 of 158 strings)

Translated using Weblate (Italian)

Currently translated at 69.6% (110 of 158 strings)

Translated using Weblate (English)

Currently translated at 100.0% (158 of 158 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 99.3% (157 of 158 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 70.8% (112 of 158 strings)

Translated using Weblate (Italian)

Currently translated at 99.3% (157 of 158 strings)

Translated using Weblate (English)

Currently translated at 100.0% (158 of 158 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: UUBulb <uub@kuzu.uk>
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/en/
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/it/
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/zh_Hant/
Translation: Nezha/Admin frontend

* chore: auto-fix linting and formatting issues

---------

Co-authored-by: UUBulb <uub@kuzu.uk>
Co-authored-by: weblate <weblate@users.noreply.github.com>
2024-12-14 16:59:45 +08:00
naiba 97d49de6be refactor: enum i18n 2024-12-14 10:39:07 +08:00
仓鼠 e0305723c3 fix: Duplicate template options (#51)
* fix: Duplicate template options

* fix: form option i18n text
2024-12-14 10:16:33 +08:00
仓鼠andhamster1963 540bb0185d Dashboard Redesign (#48)
* feat: add user_template setting

* style: header

* style: page padding

* style: header

* feat: header now time

* style: login page

* feat: nav indicator

* style: button inset shadow

* style: footer text size

* feat: header show login_ip

* fix: error toast

* fix: frontend_templates setting

* fix: lint

* feat: pr auto format

* chore: auto-fix linting and formatting issues

---------

Co-authored-by: hamster1963 <hamster1963@users.noreply.github.com>
2024-12-13 23:51:33 +08:00
仓鼠 811835ffe5 feat: pr auto format (#50) 2024-12-13 23:16:09 +08:00
UUBulb aa53b8cedc fix: custom_nameservers should be dns_servers (#46)
* fix: custom_nameservers should be dns_servers

* fix ipv6 address conversion
2024-12-13 22:27:05 +08:00
naiba dc2f28979f feat: api update 2024-12-10 22:19:31 +08:00
dca8ec9b3f Translated using Weblate (Chinese (Traditional Han script)) (#44)
Currently translated at 100.0% (156 of 156 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (156 of 156 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (156 of 156 strings)

Translated using Weblate (English)

Currently translated at 100.0% (156 of 156 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (156 of 156 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (156 of 156 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (156 of 156 strings)

Translated using Weblate (English)

Currently translated at 100.0% (156 of 156 strings)

Deleted translation using Weblate (English (United States))

Added translation using Weblate (English (United States))




Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/en/
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/it/
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/zh_Hant/
Translation: Nezha/Admin frontend

Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: UUBulb <uub@kuzu.uk>
2024-12-10 21:58:16 +08:00
naiba e2027a0ef0 fix: local debug 2024-12-09 23:11:17 +08:00
UUBulb b53688b4f4 fix: multi select scroll (#43) 2024-12-09 20:22:23 +08:00
仓鼠 982ecc7086 feat: add user_template setting (#36) 2024-12-07 01:17:00 +08:00
UUBulb 57472ca2b4 improve styles (#34) 2024-12-06 22:21:50 +08:00
UUBulb d21daa47f9 update service api (#29)
* update service api

* update api.ts
2024-12-05 21:00:48 +08:00
UUBulb 9cc1a1e1bd chore: windows env typo (#27) 2024-12-05 17:04:49 +08:00
bobyasasas 188b7b9a0e Update install-commands.tsx (#25)
修正了实际windows复制安装脚本不可用的问题
2024-12-05 00:10:58 +08:00
UUBulb 950318417f fix: missing notification group input in settings (#24)
* fix: missing notification group input in settings

* change script name

* license

* update settings api

* improve: cell width

* feat: site name
2024-12-04 20:03:16 +08:00
naiba 2e814e6f5e fix: websocket 2024-12-02 23:51:24 +08:00
naiba 4cdb11059a fix: websocket 2024-12-02 22:10:07 +08:00
UUBulb dec642a8e6 fix: profile input auto-complete (#18) 2024-12-02 20:57:19 +08:00
GuGuGu 14f4d03320 fix: Kill auto language (#21)
* fix: Kill auto language

Kill the background automatic recognition of language to prevent backend
Language file correction

* refactor(Aggregate): Ports and install hosts

Optimize multi-language

* fix(I18n): Kill spurious shortcuts

Fix wrong import
2024-12-02 20:57:03 +08:00
naiba 7f3f6e8af1 chore: i18n 2024-12-01 21:50:59 +08:00
GuGuGu dd5cf051d5 fix: language types error (#14) 2024-11-30 23:56:30 +08:00
naiba c49bbb54e1 fix: waf page 2024-11-30 21:31:55 +08:00
naiba ef70277ff4 feat: 优化复制安装命令提示 2024-11-30 20:14:49 +08:00
Colin a8f7c15f6c fix(I18n): Translation supplement
Translation supplement, automatic lanSupplement the missing translations
2024-11-30 13:04:34 +01:00
naiba a3c9fcec73 fix: copy to clipboard 2024-11-30 17:08:47 +08:00
UUBulb 31c0e4c485 fix: wrong variable name (#12) 2024-11-30 15:18:41 +08:00
UUBulbandnaiba f3a5f04392 fix: never write undefined (#11)
Co-authored-by: naiba <hi@nai.ba>
2024-11-30 15:04:00 +08:00
naiba c98632bc17 feat: 修改用户名,简化代码 2024-11-30 15:02:42 +08:00
UUBulb e42910ad35 implement install-commands button (#10)
* fix: type conversion

* implement install-commands button
2024-11-30 13:44:17 +08:00
naiba 3e9a75125a feat: 调整安装脚本复制位置 2024-11-30 12:58:53 +08:00
UUBulb f6656a8e84 fix: type conversion (#9) 2024-11-30 11:41:50 +08:00
GuGuGu 24286a7449 feat(I18n): Add multiple languages ​​(zh-CN/zh-TW/en/it) (#8) 2024-11-29 20:47:09 +08:00
UUBulb 7759c3e9ad fix: server api schema (#6) 2024-11-28 17:06:37 +08:00
仓鼠 5a87ea83d8 style: error-page (#4) 2024-11-26 21:22:56 +08:00
UUBulb 3f3a585a62 fix validation & implement profile page (#5)
* fix validation

* implement profile page

* catch error
2024-11-26 21:20:56 +08:00
UUBulb 3d1778f89d temporary fix for high cpu usage (#3) 2024-11-26 09:29:39 +08:00
Weilong Huang 6a5ab161bb Optimize loading and style (#2) 2024-11-25 13:40:28 +08:00
naiba 48cdd6f4d5 Merge pull request #1 from uubulb/dev 2024-11-25 13:37:19 +08:00
uubulb fd69492620 build: manual chunking 2024-11-25 01:31:34 +08:00
UUBulb 3b5023061b implement setting page (#12) 2024-11-23 21:59:48 +08:00
naiba f17bdbf760 fix: logout & login page home button 2024-11-23 10:25:54 +08:00
naiba 135cdf1111 rename 2024-11-23 01:18:01 +08:00
UUBulb 1c8494cd59 feat: responsive fm card (#11)
* feat: responsive fm card

* delete meaningless words

* fix joinIP
2024-11-22 22:15:41 +08:00
UUBulb b1b8b91fb5 feat: responsive header, search boxes (#10) 2024-11-22 07:41:47 +08:00
UUBulb 8248d6c910 implement remaining features of the server page (#9)
* implement remaining features of the server page

* fix fm init

* ?
2024-11-21 22:20:14 +08:00
naiba be2f5a519f chore: fetch depth 0 2024-11-20 22:32:02 +08:00
naiba 48ea9ca76a fix: build ignore error 2024-11-20 22:29:08 +08:00
naiba 4c93d1643b release 2024-11-20 22:04:08 +08:00
UUBulb 059c33eda4 implement notification page (#8) 2024-11-20 21:37:03 +08:00
UUBulb 0e4bcd3938 implement cron page (#7) 2024-11-20 00:19:40 +08:00
UUBulb a487e6cb99 implement group page (#6)
* implement group page

* group state, search box

* rename some field

* update api types
2024-11-19 21:40:03 +08:00
UUBulb 134cd670ad implement nat & ddns page (#5) 2024-11-19 14:55:21 +08:00
UUBulb bf7ffd36e0 further implementing server page (#4)
* further implementing server page

* optimize icon button

* rename some unnecessary file extensions

* add terminal page & fm card
2024-11-18 20:48:30 +08:00
UUBulb f4fbc9f488 further implementing service page (#3) 2024-11-17 10:05:20 +08:00
naiba 0c38d52025 Merge pull request #2 from uubulb/dev
init service page
2024-11-15 23:06:26 +08:00
116 changed files with 11772 additions and 8206 deletions
+21
View File
@@ -0,0 +1,21 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
groups:
npm-dependencies:
patterns:
- "*"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
groups:
github-actions:
patterns:
- "*"
+4 -4
View File
@@ -10,14 +10,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
fetch-depth: 0
ref: ${{ github.head_ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Set up Bun
uses: oven-sh/setup-bun@v1
uses: oven-sh/setup-bun@v2
with:
bun-version: "latest"
@@ -34,7 +34,7 @@ jobs:
- name: Commit and push changes
if: steps.check_changes.outputs.has_changes == 'true' || env.has_changes == 'true'
uses: stefanzweifel/git-auto-commit-action@v5
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "chore: auto-fix linting and formatting issues"
commit_options: "--no-verify"
@@ -42,7 +42,7 @@ jobs:
- name: Add PR comment
if: steps.check_changes.outputs.has_changes == 'true' || env.has_changes == 'true'
uses: actions/github-script@v7
uses: actions/github-script@v9
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
+175
View File
@@ -0,0 +1,175 @@
name: E2E
on:
push:
branches:
- main
paths:
- "src/**"
- "tests/e2e/**"
- "playwright.config.ts"
- "package.json"
- "package-lock.json"
- ".github/workflows/e2e.yml"
pull_request:
branches:
- main
jobs:
playwright:
runs-on: ubuntu-latest
timeout-minutes: 25
env:
AGENT_REPO: nezhahq/agent
AGENT_REF: main
NEZHA_REPO: nezhahq/nezha
NEZHA_REF: master
NZ_LISTENHOST: 127.0.0.1
NZ_LISTENPORT: "8008"
NZ_AGENTSECRETKEY: e2e-agent-secret-32-bytes-long-ok
NZ_JWTSECRETKEY: e2e-jwt-secret-key-min-32-chars-long-ok
NZ_SITENAME: nezha-e2e
NZ_DEBUG: "true"
E2E_ADMIN_USER: admin
E2E_ADMIN_PASS: admin
E2E_AGENT_UUID: 11111111-2222-4333-8444-555555555555
E2E_BASE_URL: http://127.0.0.1:5173
steps:
- name: Checkout admin-frontend
uses: actions/checkout@v7
with:
path: admin-frontend
- name: Checkout nezha backend
uses: actions/checkout@v7
with:
repository: ${{ env.NEZHA_REPO }}
ref: ${{ env.NEZHA_REF }}
path: nezha
- name: Checkout Nezha Agent
uses: actions/checkout@v7
with:
repository: ${{ env.AGENT_REPO }}
ref: ${{ env.AGENT_REF }}
path: agent
- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: 20
cache: npm
cache-dependency-path: admin-frontend/package-lock.json
- name: Setup Go
uses: actions/setup-go@v7
with:
go-version: "1.26.x"
cache-dependency-path: |
agent/go.sum
nezha/go.sum
- name: Install frontend dependencies
working-directory: admin-frontend
run: npm ci
- name: Install Playwright browsers
working-directory: admin-frontend
run: npx playwright install --with-deps chromium
- name: Prepare backend stubs (dist + swagger docs)
working-directory: nezha
run: |
mkdir -p cmd/dashboard/admin-dist cmd/dashboard/user-dist
printf '<!doctype html><title>admin e2e</title>\n' > cmd/dashboard/admin-dist/index.html
printf '<!doctype html><title>user e2e</title>\n' > cmd/dashboard/user-dist/index.html
go install github.com/swaggo/swag/cmd/swag@latest
"$(go env GOPATH)/bin/swag" init --pd -d cmd/dashboard -g main.go -o cmd/dashboard/docs
go build -o /tmp/nezha-dashboard ./cmd/dashboard
- name: Build Agent
working-directory: agent
run: go build -o /tmp/nezha-agent ./cmd/agent
- name: Start backend
working-directory: nezha
env:
GIN_MODE: release
run: |
mkdir -p data
nohup /tmp/nezha-dashboard -c data/config.yaml -db data/sqlite.db \
> /tmp/dashboard.log 2>&1 &
echo $! > /tmp/dashboard.pid
- name: Wait for backend health
run: |
for i in {1..60}; do
if curl -fsS http://127.0.0.1:8008/api/v1/setting > /dev/null; then
echo "backend is up"
exit 0
fi
sleep 1
done
echo "backend failed to start within 60s"
echo "---- dashboard.log ----"
cat /tmp/dashboard.log || true
exit 1
- name: Start Agent
working-directory: agent
env:
NZ_SERVER: 127.0.0.1:8008
NZ_CLIENT_SECRET: ${{ env.NZ_AGENTSECRETKEY }}
NZ_UUID: ${{ env.E2E_AGENT_UUID }}
NZ_TLS: "false"
NZ_DISABLE_AUTO_UPDATE: "true"
run: |
nohup /tmp/nezha-agent -c /tmp/nezha-agent-config.yml \
> /tmp/agent.log 2>&1 &
echo $! > /tmp/agent.pid
- name: Run Playwright tests
working-directory: admin-frontend
env:
CI: "true"
run: npm run e2e
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v7
with:
name: playwright-report
path: admin-frontend/playwright-report
retention-days: 14
- name: Upload dashboard log
if: always()
uses: actions/upload-artifact@v7
with:
name: dashboard-log
path: /tmp/dashboard.log
retention-days: 7
- name: Upload Agent log
if: always()
uses: actions/upload-artifact@v7
with:
name: agent-log
path: /tmp/agent.log
retention-days: 7
- name: Stop Agent
if: always()
run: |
if [ -f /tmp/agent.pid ]; then
kill "$(cat /tmp/agent.pid)" || true
fi
- name: Stop backend
if: always()
run: |
if [ -f /tmp/dashboard.pid ]; then
kill "$(cat /tmp/dashboard.pid)" || true
fi
+37
View File
@@ -0,0 +1,37 @@
name: Frontend CI
on:
pull_request:
branches:
- main
push:
branches:
- main
jobs:
build-and-test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: 22.19.0
cache: npm
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm ci
- name: Run lint
run: npm run lint
- name: Build frontend
run: npm run build
- name: Run unit tests
run: npm run test
+3 -3
View File
@@ -12,12 +12,12 @@ jobs:
contents: write
steps:
- name: Check out code
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Bun
uses: oven-sh/setup-bun@v1
uses: oven-sh/setup-bun@v2
with:
bun-version: "latest"
@@ -32,7 +32,7 @@ jobs:
run: zip -r dist.zip dist
- name: Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
files: dist.zip
generate_release_notes: true
+6
View File
@@ -24,3 +24,9 @@ dist-ssr
*.sw?
bun.lock
pnpm-lock.yaml
# Playwright
/playwright-report
/test-results
/blob-report
/playwright/.cache
+89
View File
@@ -0,0 +1,89 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"categories": {
"correctness": "error"
},
"env": {
"browser": true
},
"ignorePatterns": ["dist"],
"plugins": ["eslint", "typescript", "react"],
"rules": {
"no-case-declarations": "error",
"no-empty": "error",
"no-fallthrough": "error",
"no-prototype-builtins": "error",
"no-regex-spaces": "error",
"no-unexpected-multiline": "error",
"no-useless-assignment": "error",
"preserve-caught-error": "error",
"no-array-constructor": "error",
"no-var": "error",
"prefer-const": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"typescript/ban-ts-comment": "error",
"typescript/no-empty-object-type": "error",
"typescript/no-namespace": "error",
"typescript/no-require-imports": "error",
"typescript/no-unnecessary-type-constraint": "error",
"typescript/no-unsafe-function-type": "error",
"react/rules-of-hooks": "error",
"typescript/no-explicit-any": "off",
"react/only-export-components": "off",
"react/immutability": "off",
"react/set-state-in-effect": "off",
"react/incompatible-library": "off",
"react/refs": "off"
},
"overrides": [
{
"files": [
"vite.config.ts",
"vitest.config.ts",
"playwright.config.ts",
"postcss.config.js",
"tailwind.config.js"
],
"env": {
"browser": false,
"node": true
}
},
{
"files": ["src/test/**/*.{ts,tsx}"],
"env": {
"browser": true,
"node": true,
"vitest": true
}
},
{
"files": ["tests/e2e/**/*.ts"],
"env": {
"browser": false,
"node": true
}
},
{
"files": [
"src/components/server-config.tsx",
"src/components/server-config-batch.tsx",
"src/routes/server.tsx"
],
"rules": {
// Existing migration debt remains visible without blocking lint.
"no-useless-assignment": "warn"
}
},
{
"files": ["tests/e2e/fixtures.ts"],
"rules": {
// Playwright fixture callbacks are not React hooks.
"react/rules-of-hooks": "off"
}
}
]
}
+1
View File
@@ -1 +1,2 @@
src/main.tsx
.github/workflows/*
+58
View File
@@ -5,3 +5,61 @@
```bash
npx swagger-typescript-api -p http://localhost:8008/swagger/doc.json -o ./src/types -n api.ts --no-client --union-enums
```
## End-to-end tests
Playwright suite lives in `tests/e2e/` and runs in CI via
`.github/workflows/e2e.yml`. The workflow checks out the backend
(`nezhahq/nezha` master), starts it on `127.0.0.1:8008`, then runs
`npm run e2e` here.
### Run locally
First install browsers once:
```bash
npm run e2e:install
```
Then start the backend separately. The shape the CI workflow uses is:
```bash
# in a checkout of nezhahq/nezha
mkdir -p cmd/dashboard/admin-dist cmd/dashboard/user-dist
printf '<!doctype html><title>admin e2e</title>\n' > cmd/dashboard/admin-dist/index.html
printf '<!doctype html><title>user e2e</title>\n' > cmd/dashboard/user-dist/index.html
go install github.com/swaggo/swag/cmd/swag@latest
"$(go env GOPATH)/bin/swag" init --pd -d cmd/dashboard -g main.go -o cmd/dashboard/docs
go build -o /tmp/nezha-dashboard ./cmd/dashboard
NZ_LISTENHOST=127.0.0.1 NZ_LISTENPORT=8008 \
NZ_JWTSECRETKEY=e2e-jwt-secret-key-min-32-chars-long-ok \
NZ_AGENTSECRETKEY=e2e-agent-secret-32-bytes-long-ok \
NZ_SITENAME=nezha-e2e GIN_MODE=release \
/tmp/nezha-dashboard -c data/config.yaml -db data/sqlite.db
```
Then back in this repo:
```bash
npm run e2e # Vite dev server starts automatically
npm run e2e -- --ui # interactive runner for debugging
```
### Reusing a running stack
If you already have Vite (`npm run dev`) and the backend up:
```bash
E2E_SKIP_WEBSERVER=1 E2E_BASE_URL=http://localhost:5173 npm run e2e
```
Override credentials for a non-default admin account with `E2E_ADMIN_USER`
and `E2E_ADMIN_PASS`.
### Triaging a CI failure
The workflow uploads two artifacts on failure:
- `playwright-report` — HTML report; open `index.html` locally.
- `dashboard-log` — full backend stdout/stderr for the test run.
-27
View File
@@ -1,27 +0,0 @@
import js from "@eslint/js"
import reactHooks from "eslint-plugin-react-hooks"
import reactRefresh from "eslint-plugin-react-refresh"
import globals from "globals"
import tseslint from "typescript-eslint"
export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
"@typescript-eslint/no-explicit-any": "off",
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
indent: ["error", 4],
},
},
)
+2976 -4381
View File
File diff suppressed because it is too large Load Diff
+62 -59
View File
@@ -7,78 +7,81 @@
"dev": "vite",
"build": "tsc -b && vite build",
"build-ignore-error": "vite build",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"lint": "oxlint .",
"lint:fix": "oxlint --fix .",
"format": "prettier --write .",
"preview": "vite preview"
"test": "vitest run",
"preview": "vite preview",
"e2e": "playwright test",
"e2e:install": "playwright install --with-deps chromium"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.3",
"@hookform/resolvers": "^5.9.1",
"@radix-ui/react-alert-dialog": "^1.1.23",
"@radix-ui/react-avatar": "^1.2.6",
"@radix-ui/react-checkbox": "^1.3.11",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@tailwindcss/postcss": "^4.1.14",
"@tanstack/react-table": "^8.21.3",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/luxon": "^3.7.1",
"@xterm/addon-attach": "^0.11.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"@radix-ui/react-dropdown-menu": "^2.1.24",
"@radix-ui/react-label": "^2.1.15",
"@radix-ui/react-navigation-menu": "^1.2.22",
"@radix-ui/react-popover": "^1.1.23",
"@radix-ui/react-scroll-area": "^1.2.18",
"@radix-ui/react-select": "^2.3.7",
"@radix-ui/react-separator": "^1.1.15",
"@radix-ui/react-slot": "^1.3.3",
"@radix-ui/react-switch": "^1.3.7",
"@radix-ui/react-tabs": "^1.1.21",
"@tailwindcss/postcss": "^4.3.3",
"@tanstack/react-table": "^9.1.2",
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/luxon": "^3.7.5",
"@xterm/addon-fit": "^0.11.0",
"@xterm/xterm": "^6.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"copy-to-clipboard": "^3.3.3",
"date-fns": "^4.1.0",
"framer-motion": "^12.23.22",
"i18next": "^25.5.3",
"i18next-browser-languagedetector": "^8.2.0",
"copy-to-clipboard": "^4.0.2",
"date-fns": "^4.4.0",
"framer-motion": "^13.1.1",
"i18next": "^26.4.0",
"i18next-browser-languagedetector": "^8.2.1",
"jotai-zustand": "^0.6.0",
"lucide-react": "^0.545.0",
"lucide-react": "^1.34.0",
"luxon": "^3.7.2",
"next-themes": "^0.4.6",
"prettier-plugin-tailwindcss": "^0.6.14",
"react": "^19.2.0",
"react-day-picker": "^9.11.1",
"react-dom": "^19.2.0",
"react-hook-form": "^7.71.1",
"react-i18next": "^16.0.0",
"react-router-dom": "^7.9.4",
"react-virtuoso": "^4.14.1",
"sonner": "^2.0.7",
"swr": "^2.3.6",
"tailwind-merge": "^3.3.1",
"prettier-plugin-tailwindcss": "^0.8.1",
"react": "^19.2.8",
"react-day-picker": "^10.0.1",
"react-dom": "^19.2.8",
"react-hook-form": "^7.86.0",
"react-i18next": "^17.0.12",
"react-router-dom": "^7.18.2",
"react-virtuoso": "^4.18.12",
"simple-icons": "^16.28.0",
"sonner": "^2.0.8",
"swr": "^2.5.1",
"tailwind-merge": "^3.6.0",
"tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2",
"zod": "^4.1.12",
"zustand": "^5.0.8"
"zod": "^4.4.3",
"zustand": "^5.0.15"
},
"devDependencies": {
"@eslint/js": "^9.37.0",
"@types/node": "^24.7.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.1",
"@vitejs/plugin-react": "^5.0.4",
"autoprefixer": "^10.4.21",
"eslint": "^9.37.0",
"eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-react-refresh": "^0.4.23",
"globals": "^16.4.0",
"postcss": "8.4.24",
"swagger-typescript-api": "^13.2.15",
"tailwindcss": "3.4.19",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.0",
"vite": "^7.1.9"
"@playwright/test": "^1.62.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.2",
"@types/node": "^26.3.0",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.5",
"@vitejs/plugin-react": "^6.1.0",
"autoprefixer": "^10.5.4",
"jsdom": "^30.0.1",
"oxlint": "1.80.0",
"postcss": "8.5.26",
"swagger-typescript-api": "^13.12.6",
"tailwindcss": "4.3.3",
"typescript": "~7.0.2",
"vite": "^8.2.2",
"vitest": "^4.1.11"
}
}
+32
View File
@@ -0,0 +1,32 @@
import { defineConfig, devices } from "@playwright/test"
const baseURL = process.env.E2E_BASE_URL || "http://localhost:5173"
export default defineConfig({
testDir: "./tests/e2e",
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
workers: 1,
reporter: process.env.CI ? [["github"], ["html", { open: "never" }]] : "list",
use: {
baseURL,
trace: "on-first-retry",
screenshot: "only-on-failure",
video: process.env.CI ? "retain-on-failure" : "off",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
webServer: process.env.E2E_SKIP_WEBSERVER
? undefined
: {
command: "npm run dev -- --host 127.0.0.1",
url: baseURL + "/dashboard/login",
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
})
+3120 -2853
View File
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -1,6 +1,5 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
"@tailwindcss/postcss": {},
},
}
+210
View File
@@ -0,0 +1,210 @@
import { FetcherMethod, fetcher } from "./api"
export interface ApiTokenView {
id: number
name: string
scopes: string[]
server_ids?: number[]
expires_at?: string
last_used_at?: string
last_used_ip?: string
created_at: string
}
export interface ApiTokenCreateRequest {
name: string
scopes: string[]
server_ids?: number[]
expires_in_days?: number
}
export interface ApiTokenCreateResponse {
id: number
name: string
token: string
scopes: string[]
server_ids?: number[]
expires_at?: string
}
export const listApiTokens = async (): Promise<ApiTokenView[]> => {
const tokens = await fetcher<ApiTokenView[]>(FetcherMethod.GET, "/api/v1/api-tokens", null)
// Go encodes an empty scope slice as JSON null; coerce so callers can map() safely.
return (tokens ?? []).map((tok) => ({
...tok,
scopes: Array.isArray(tok.scopes) ? tok.scopes : [],
}))
}
export const createApiToken = async (
data: ApiTokenCreateRequest,
): Promise<ApiTokenCreateResponse> => {
return fetcher<ApiTokenCreateResponse>(FetcherMethod.POST, "/api/v1/api-tokens", data)
}
export const deleteApiToken = async (id: number): Promise<void> => {
return fetcher<void>(FetcherMethod.DELETE, `/api/v1/api-tokens/${id}`, null)
}
export const SCOPE_OPTIONS = [
{
value: "nezha:inventory:read",
label: "Inventory: read",
desc: "List servers & groups (server.list)",
},
{
value: "nezha:inventory:delete",
label: "Inventory: delete",
desc: "Delete servers & server groups",
},
{ value: "nezha:inventory:*", label: "Inventory: all", desc: "List + delete servers & groups" },
{
value: "nezha:server:read",
label: "Server: read",
desc: "Inspect a server, read files & metrics (needs server id)",
},
{ value: "nezha:server:write", label: "Server: write", desc: "Edit a server, push files" },
{ value: "nezha:server:delete", label: "Server: delete", desc: "Delete files on a server" },
{ value: "nezha:server:exec", label: "Server: exec", desc: "Run shell commands on servers" },
{
value: "nezha:server:*",
label: "Server: all",
desc: "Every server operation (read+write+delete+exec)",
},
{
value: "nezha:service:read",
label: "Service monitor: read",
desc: "List service monitors & history",
},
{
value: "nezha:service:write",
label: "Service monitor: write",
desc: "Create / edit service monitors",
},
{
value: "nezha:service:delete",
label: "Service monitor: delete",
desc: "Delete service monitors",
},
{
value: "nezha:service:*",
label: "Service monitor: all",
desc: "Every service monitor permission",
},
{ value: "nezha:alertrule:read", label: "Alert rule: read", desc: "List alert rules" },
{
value: "nezha:alertrule:write",
label: "Alert rule: write",
desc: "Create / edit alert rules",
},
{ value: "nezha:alertrule:delete", label: "Alert rule: delete", desc: "Delete alert rules" },
{ value: "nezha:alertrule:*", label: "Alert rule: all", desc: "Every alert-rule permission" },
{ value: "nezha:cron:read", label: "Cron: read", desc: "List scheduled tasks" },
{ value: "nezha:cron:write", label: "Cron: write", desc: "Create / edit scheduled tasks" },
{ value: "nezha:cron:delete", label: "Cron: delete", desc: "Delete scheduled tasks" },
{ value: "nezha:cron:exec", label: "Cron: trigger", desc: "Manually trigger scheduled tasks" },
{ value: "nezha:cron:*", label: "Cron: all", desc: "Every cron permission" },
{ value: "nezha:notification:read", label: "Notification: read", desc: "List notifications" },
{
value: "nezha:notification:write",
label: "Notification: write",
desc: "Create / edit notifications",
},
{
value: "nezha:notification:delete",
label: "Notification: delete",
desc: "Delete notifications",
},
{
value: "nezha:notification:*",
label: "Notification: all",
desc: "Every notification permission",
},
{
value: "nezha:notification-group:read",
label: "Notification group: read",
desc: "List notification groups",
},
{
value: "nezha:notification-group:write",
label: "Notification group: write",
desc: "Create / edit groups",
},
{
value: "nezha:notification-group:delete",
label: "Notification group: delete",
desc: "Delete groups",
},
{
value: "nezha:notification-group:*",
label: "Notification group: all",
desc: "Every notification-group permission",
},
{ value: "nezha:ddns:read", label: "DDNS: read", desc: "List DDNS profiles" },
{ value: "nezha:ddns:write", label: "DDNS: write", desc: "Create / edit DDNS profiles" },
{ value: "nezha:ddns:delete", label: "DDNS: delete", desc: "Delete DDNS profiles" },
{ value: "nezha:ddns:*", label: "DDNS: all", desc: "Every DDNS permission" },
{ value: "nezha:nat:read", label: "NAT: read", desc: "List NAT rules" },
{ value: "nezha:nat:write", label: "NAT: write", desc: "Create / edit NAT rules" },
{ value: "nezha:nat:delete", label: "NAT: delete", desc: "Delete NAT rules" },
{ value: "nezha:nat:*", label: "NAT: all", desc: "Every NAT permission" },
{ value: "nezha:transfer:read", label: "Transfer: read", desc: "Read server transfer state" },
{ value: "nezha:transfer:write", label: "Transfer: write", desc: "Cancel / retry transfers" },
{
value: "nezha:transfer:delete",
label: "Transfer: delete",
desc: "Delete server transfer records",
},
{ value: "nezha:transfer:*", label: "Transfer: all", desc: "Every transfer permission" },
{
value: "nezha:admin:*",
label: "Admin: all (admin only)",
desc: "User / WAF / Setting / Online-user management",
},
{ value: "nezha:*", label: "Everything (admin only)", desc: "Full access to all resources" },
] as const
export type Scope = (typeof SCOPE_OPTIONS)[number]["value"]
export type ParseServerIDsResult =
{ ok: true; value: number[] | undefined } | { ok: false; error: string }
export type ParseExpiresInDaysResult =
{ ok: true; value: number | undefined } | { ok: false; error: string }
// Validates the "expires in days" field before it is sent to the backend.
// The backend model field is `ExpiresInDays int`, so a fractional value would
// fail JSON binding; reject it locally. Blank and 0 both mean "never expires"
// (undefined), matching the create handler's `expires_in_days == 0` semantics.
export function parseExpiresInDaysInput(raw: string): ParseExpiresInDaysResult {
const trimmed = raw.trim()
if (trimmed === "") return { ok: true, value: undefined }
if (!/^\d+$/.test(trimmed)) return { ok: false, error: `invalid expiry: ${raw}` }
const n = Number(trimmed)
if (!Number.isInteger(n) || n < 0 || n > 3650) {
return { ok: false, error: `invalid expiry: ${raw}` }
}
return { ok: true, value: n > 0 ? n : undefined }
}
export function parseServerIDsInput(raw: string): ParseServerIDsResult {
const trimmed = raw.trim()
if (trimmed === "") return { ok: true, value: undefined }
const parts = trimmed.split(",").map((s) => s.trim())
const out: number[] = []
const seen = new Set<number>()
for (const p of parts) {
if (p === "") return { ok: false, error: "empty server id" }
if (!/^\d+$/.test(p)) return { ok: false, error: `invalid server id: ${p}` }
const n = Number(p)
// Server IDs are uint64 on the backend; reject values that lose
// precision as a JS number, otherwise the PAT could bind to a
// different server than the operator typed.
if (!Number.isSafeInteger(n) || n <= 0)
return { ok: false, error: `invalid server id: ${p}` }
if (seen.has(n)) continue
seen.add(n)
out.push(n)
}
return { ok: true, value: out }
}
+67 -6
View File
@@ -23,17 +23,56 @@ export enum FetcherMethod {
let lastestRefreshTokenAt = 0
const csrfCookieName = "nz-csrf"
const csrfHeaderName = "X-CSRF-Token"
function readCookie(name: string): string {
const prefix = name + "="
for (const part of document.cookie.split(";")) {
const c = part.trim()
if (c.startsWith(prefix)) return c.slice(prefix.length)
}
return ""
}
function isUnsafeMethod(method: FetcherMethod): boolean {
return method !== FetcherMethod.GET
}
// Only attach the CSRF token to same-origin requests. Keying on HTTP method
// alone would leak the nz-csrf value to any absolute cross-origin URL a caller
// passes in; the double-submit token is meaningful only to our own backend.
function isSameOrigin(path: string): boolean {
try {
return new URL(path, window.location.origin).origin === window.location.origin
} catch {
return false
}
}
// Double-submit CSRF: backend requires X-CSRF-Token == nz-csrf cookie on
// cookie-authenticated unsafe methods.
function csrfHeaders(method: FetcherMethod, path: string): Record<string, string> {
if (!isUnsafeMethod(method)) return {}
if (!isSameOrigin(path)) return {}
const token = readCookie(csrfCookieName)
return token ? { [csrfHeaderName]: token } : {}
}
export async function fetcher<T>(method: FetcherMethod, path: string, data?: any): Promise<T> {
let response
if (method === FetcherMethod.GET || method === FetcherMethod.DELETE) {
response = await fetch(buildUrl(path, data), {
method: method,
headers: csrfHeaders(method, path),
})
} else {
response = await fetch(path, {
method: method,
headers: {
"Content-Type": "application/json",
...csrfHeaders(method, path),
},
body: data ? JSON.stringify(data) : null,
})
@@ -41,23 +80,45 @@ export async function fetcher<T>(method: FetcherMethod, path: string, data?: any
if (!response.ok) {
throw new Error(response.statusText)
}
const responseData: CommonResponse<T> = await response.json()
const text = await response.text()
if (text !== "") {
let responseData: CommonResponse<T>
try {
responseData = JSON.parse(text)
} catch {
throw new Error("invalid server response")
}
if (!responseData.success) {
throw new Error(responseData.error)
}
triggerAutoRefresh()
return responseData.data
}
triggerAutoRefresh()
return undefined as T
}
// auto refresh token
// Refresh route is POST behind the CSRF gate. Defer until an nz-csrf cookie is
// available: firing without the header would only 403 and burn the 1h throttle,
// stranding sessions that predate the cookie until the backend seeds one on a
// safe GET.
function triggerAutoRefresh() {
if (!readCookie(csrfCookieName)) return
if (
document.cookie &&
(!lastestRefreshTokenAt || Date.now() - lastestRefreshTokenAt > 1000 * 60 * 60)
) {
lastestRefreshTokenAt = Date.now()
fetch("/api/v1/refresh-token")
fetch("/api/v1/refresh-token", {
method: "POST",
headers: csrfHeaders(FetcherMethod.POST, "/api/v1/refresh-token"),
})
}
return responseData.data
}
export async function swrFetcher<T>(input: string | URL | globalThis.Request, init?: RequestInit) {
return fetcher<T>(init?.method as FetcherMethod, input.toString(), init?.body)
// SWR 默认不带 initmethod 为 undefined:必须落到 GET,否则 fetcher 会走
// 带 body 的分支并对只读请求附加 CSRF 头,把 token 暴露到 GET 请求上。
const method = (init?.method as FetcherMethod) ?? FetcherMethod.GET
return fetcher<T>(method, input.toString(), init?.body)
}
+1 -1
View File
@@ -15,5 +15,5 @@ export const deleteCron = async (id: number[]): Promise<void> => {
}
export const runCron = async (id: number): Promise<void> => {
return fetcher<void>(FetcherMethod.GET, `/api/v1/cron/${id}/manual`, null)
return fetcher<void>(FetcherMethod.POST, `/api/v1/cron/${id}/manual`, null)
}
+1 -1
View File
@@ -3,5 +3,5 @@ import { ModelCreateFMResponse } from "@/types"
import { FetcherMethod, fetcher } from "./api"
export const createFM = async (id: string): Promise<ModelCreateFMResponse> => {
return fetcher<ModelCreateFMResponse>(FetcherMethod.GET, `/api/v1/file?id=${id}`, null)
return fetcher<ModelCreateFMResponse>(FetcherMethod.POST, `/api/v1/file?id=${id}`, null)
}
+15 -2
View File
@@ -1,5 +1,6 @@
import {
ModelBatchMoveServerForm,
ModelBatchMoveServerResult,
ModelServer,
ModelServerConfigForm,
ModelServerForm,
@@ -16,8 +17,20 @@ export const deleteServer = async (id: number[]): Promise<void> => {
return fetcher<void>(FetcherMethod.POST, "/api/v1/batch-delete/server", id)
}
export const batchMoveServer = async (data: ModelBatchMoveServerForm): Promise<void> => {
return fetcher<void>(FetcherMethod.POST, "/api/v1/batch-move/server", data)
// batchMoveServer kicks off one ServerTransfer per id and returns a per-id
// result. The dashboard previously returned void from this endpoint; the new
// response shape carries the transfer ID for callers that want to subscribe
// to /ws/transfer for state changes, plus structured non-pending statuses
// (permission_denied, already_transferring, server_not_found, same_owner)
// that the UI can render without parsing prose error messages.
export const batchMoveServer = async (
data: ModelBatchMoveServerForm,
): Promise<ModelBatchMoveServerResult[]> => {
return fetcher<ModelBatchMoveServerResult[]>(
FetcherMethod.POST,
"/api/v1/batch-move/server",
data,
)
}
export const forceUpdateServer = async (id: number[]): Promise<ModelServerTaskResponse> => {
+15
View File
@@ -0,0 +1,15 @@
import { ModelServerTransfer } from "@/types"
import { FetcherMethod, fetcher } from "./api"
export const getServerTransfers = async (): Promise<ModelServerTransfer[]> => {
return fetcher<ModelServerTransfer[]>(FetcherMethod.GET, "/api/v1/transfer", null)
}
export const cancelServerTransfer = async (id: number): Promise<ModelServerTransfer> => {
return fetcher<ModelServerTransfer>(FetcherMethod.POST, `/api/v1/transfer/${id}/cancel`)
}
export const retryServerTransfer = async (id: number): Promise<ModelServerTransfer> => {
return fetcher<ModelServerTransfer>(FetcherMethod.POST, `/api/v1/transfer/${id}/retry`)
}
+33 -22
View File
@@ -36,7 +36,7 @@ import { ModelAlertRule } from "@/types"
import { triggerModes } from "@/types"
import { zodResolver } from "@hookform/resolvers/zod"
import { useEffect, useState } from "react"
import { useForm } from "react-hook-form"
import { useForm, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { KeyedMutator } from "swr"
@@ -50,13 +50,15 @@ interface AlertRuleCardProps {
mutate: KeyedMutator<ModelAlertRule[]>
}
const cycleUnitSchema = z.enum(["hour", "day", "week", "month", "year"])
const ruleSchema = z.object({
type: z.string(),
min: z.number().optional(),
max: z.number().optional(),
cycle_start: z.string().optional(),
cycle_interval: z.number().optional(),
cycle_unit: z.enum(["hour", "day", "week", "month", "year"]).optional(),
cycle_unit: cycleUnitSchema.optional(),
duration: z.number().optional(),
cover: z.number().int().min(0),
ignore: z.record(z.string(), z.boolean()).optional(),
@@ -71,7 +73,7 @@ const alertRuleFormSchema = z.object({
try {
JSON.parse(val)
return true
} catch (e) {
} catch {
return false
}
},
@@ -92,10 +94,12 @@ const alertRuleFormSchema = z.object({
export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) => {
const { t } = useTranslation()
type AlertRuleFormData = z.infer<typeof alertRuleFormSchema>
type AlertRuleEntry = z.output<typeof ruleSchema>
type AlertRuleFormInput = z.input<typeof alertRuleFormSchema>
type AlertRuleFormData = z.output<typeof alertRuleFormSchema>
const form = useForm({
resolver: zodResolver(alertRuleFormSchema) as any,
const form = useForm<AlertRuleFormInput, unknown, AlertRuleFormData>({
resolver: zodResolver(alertRuleFormSchema),
defaultValues: data
? {
...data,
@@ -124,14 +128,16 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
// 结构化规则编辑状态:从已有数据或 rules_raw 初始化
const initialRules = (() => {
try {
if (data?.rules) return data.rules as any[]
if (data?.rules) return data.rules
const raw = form.getValues("rules_raw")
return raw ? JSON.parse(raw) : []
if (!raw) return []
const parsed: unknown = JSON.parse(raw)
return z.array(ruleSchema).parse(parsed)
} catch {
return []
}
})()
const [rulesUI, setRulesUI] = useState<any[]>(initialRules)
const [rulesUI, setRulesUI] = useState<AlertRuleEntry[]>(initialRules)
// 同步到 rules_raw(提交仍走 JSON 字符串)
useEffect(() => {
@@ -140,17 +146,22 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
} catch {
// ignore
}
}, [rulesUI])
}, [form, rulesUI])
const rulesRaw = useWatch({ control: form.control, name: "rules_raw" })
const onSubmit = async (values: AlertRuleFormData) => {
values.rules = JSON.parse(values.rules_raw)
values.rules = z.array(ruleSchema).parse(JSON.parse(values.rules_raw))
values.fail_trigger_tasks = conv.strToArr(values.fail_trigger_tasks_raw).map(Number)
values.recover_trigger_tasks = conv.strToArr(values.recover_trigger_tasks_raw).map(Number)
const { rules_raw, ...requiredFields } = values
const requiredFields = { ...values }
delete (requiredFields as Record<string, unknown>).rules_raw
try {
data?.id
? await updateAlertRule(data.id, requiredFields)
: await createAlertRule(requiredFields)
if (data?.id) {
await updateAlertRule(data.id, requiredFields)
} else {
await createAlertRule(requiredFields)
}
} catch (e) {
console.error(e)
toast(t("Error"), {
@@ -184,10 +195,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit as any)}
className="space-y-2 my-2"
>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 my-2">
<FormField
control={form.control}
name="name"
@@ -531,7 +539,10 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
const next = [...rulesUI]
next[idx] = {
...next[idx],
cycle_unit: val,
cycle_unit:
cycleUnitSchema.parse(
val,
),
}
setRulesUI(next)
}}
@@ -603,7 +614,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
<FormControl>
<Textarea
className="resize-y"
value={form.watch("rules_raw")}
value={rulesRaw}
onChange={(e) => {
// 同步到结构化编辑器
form.setValue("rules_raw", e.target.value, {
@@ -630,7 +641,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
placeholder={t("Search")}
options={ngroupList}
onValueChange={field.onChange}
defaultValue={field.value.toString()}
defaultValue={String(field.value ?? "")}
/>
</FormControl>
<FormMessage />
+5 -6
View File
@@ -87,14 +87,13 @@ export const BatchEditNoteIcon: React.FC<BatchEditNoteIconProps> = ({
// For batch, usually we only override if they typed something.
// We can provide a checkbox or just update if not empty.
// Let's just update if not empty for safety.
let updatePayload = { ...server }
if (values.note && values.note.trim() !== "") {
updatePayload.note = values.note
}
if (values.public_note && values.public_note.trim() !== "") {
updatePayload.public_note = values.public_note
const updatePayload = {
...server,
note: values.note && values.note.trim() !== "" ? values.note : server.note,
public_note: values.public_note && values.public_note.trim() !== "" ? values.public_note : server.public_note,
}
// Clean up non-API fields
return updateServer(id, {
name: updatePayload.name,
+26 -2
View File
@@ -33,8 +33,9 @@ export const BatchMoveServerIcon: React.FC<BatchMoveServerIconProps> = ({
const [toUserId, setToUserId] = useState<number | undefined>(undefined)
const onSubmit = async () => {
let results
try {
await batchMoveServer({
results = await batchMoveServer({
ids: serverIds,
to_user: toUserId!,
})
@@ -45,7 +46,30 @@ export const BatchMoveServerIcon: React.FC<BatchMoveServerIconProps> = ({
})
return
}
toast(t("Done"))
// The backend now responds per-server; render a structured summary
// instead of "Done" so the operator can see which ids hit a snag
// (DisableCommandExecute / already_transferring / etc). Pending ids
// remain async — watching /ws/transfer or the Transfers page will
// surface their terminal state.
const pending = results.filter((r) => r.status === "pending").length
const denied = results.filter((r) => r.status === "permission_denied").length
const dup = results.filter((r) => r.status === "already_transferring").length
const missing = results.filter((r) => r.status === "server_not_found").length
const same = results.filter((r) => r.status === "same_owner").length
const tooOld = results.filter((r) => r.status === "agent_too_old").length
const parts: string[] = []
if (pending) parts.push(t("Transfer.PendingCount", { count: pending }))
if (denied) parts.push(t("Transfer.PermissionDeniedCount", { count: denied }))
if (dup) parts.push(t("Transfer.AlreadyTransferringCount", { count: dup }))
if (missing) parts.push(t("Transfer.ServerNotFoundCount", { count: missing }))
if (same) parts.push(t("Transfer.SameOwnerCount", { count: same }))
if (tooOld) parts.push(t("Transfer.AgentTooOldCount", { count: tooOld }))
toast(t("Transfer.BatchSubmitted"), {
description: parts.join(" · ") || t("Done"),
})
setOpen(false)
}
+13 -8
View File
@@ -63,21 +63,22 @@ const cronFormSchema = z.object({
notification_group_id: z.coerce.number().int(),
})
type CronFormData = z.infer<typeof cronFormSchema>
type CronFormInput = z.input<typeof cronFormSchema>
type CronFormData = z.output<typeof cronFormSchema>
export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
const { t } = useTranslation()
const form = useForm<CronFormData>({
resolver: zodResolver(cronFormSchema as any),
const form = useForm<CronFormInput, unknown, CronFormData>({
resolver: zodResolver(cronFormSchema),
defaultValues: data
? {
task_type: data.task_type ?? 0,
name: data.name ?? "",
scheduler: data.scheduler ?? "",
command: (data as any).command ?? "",
command: data.command ?? "",
servers: data.servers ?? [],
cover: data.cover ?? 0,
push_successful: (data as any).push_successful ?? false,
push_successful: data.push_successful ?? false,
notification_group_id: data.notification_group_id ?? 0,
}
: {
@@ -99,7 +100,11 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
const onSubmit = async (values: CronFormData) => {
try {
data?.id ? await updateCron(data.id, values) : await createCron(values)
if (data?.id) {
await updateCron(data.id, values)
} else {
await createCron(values)
}
} catch (e) {
console.error(e)
toast(t("Error"), {
@@ -267,7 +272,7 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
placeholder="Search..."
options={ngroupList}
onValueChange={field.onChange}
defaultValue={field.value.toString()}
defaultValue={String(field.value ?? "")}
/>
</FormControl>
<FormMessage />
@@ -282,7 +287,7 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
<FormControl>
<div className="flex items-center gap-2">
<Checkbox
checked={field.value}
checked={field.value === true}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">
+31 -18
View File
@@ -67,28 +67,29 @@ const ddnsFormSchema = z.object({
webhook_headers: asOptionalField(z.string()),
})
type DDNSFormData = z.infer<typeof ddnsFormSchema>
type DDNSFormInput = z.input<typeof ddnsFormSchema>
type DDNSFormData = z.output<typeof ddnsFormSchema>
export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) => {
const { t } = useTranslation()
const form = useForm<DDNSFormData>({
resolver: zodResolver(ddnsFormSchema as any),
const form = useForm<DDNSFormInput, unknown, DDNSFormData>({
resolver: zodResolver(ddnsFormSchema),
defaultValues: data
? {
max_retries: data.max_retries ?? 3,
enable_ipv4: (data as any).enable_ipv4 ?? false,
enable_ipv6: (data as any).enable_ipv6 ?? false,
enable_ipv4: data.enable_ipv4 ?? false,
enable_ipv6: data.enable_ipv6 ?? false,
name: data.name ?? "",
provider: data.provider ?? "dummy",
domains: data.domains ?? [],
domains_raw: conv.arrToStr(data.domains ?? []),
access_id: (data as any).access_id ?? "",
access_secret: (data as any).access_secret ?? "",
webhook_url: (data as any).webhook_url ?? "",
webhook_method: (data as any).webhook_method,
webhook_request_type: (data as any).webhook_request_type,
webhook_request_body: (data as any).webhook_request_body ?? "",
webhook_headers: (data as any).webhook_headers ?? "",
access_id: data.access_id ?? "",
access_secret: data.access_secret ?? "",
webhook_url: data.webhook_url ?? "",
webhook_method: data.webhook_method,
webhook_request_type: data.webhook_request_type,
webhook_request_body: data.webhook_request_body ?? "",
webhook_headers: data.webhook_headers ?? "",
}
: {
max_retries: 3,
@@ -116,7 +117,11 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
const onSubmit = async (values: DDNSFormData) => {
try {
values.domains = conv.strToArr(values.domains_raw)
data?.id ? await updateDDNSProfile(data.id, values) : await createDDNSProfile(values)
if (data?.id) {
await updateDDNSProfile(data.id, values)
} else {
await createDDNSProfile(values)
}
} catch (e) {
console.error(e)
toast(t("Error"), {
@@ -227,15 +232,23 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
<FormField
control={form.control}
name="max_retries"
render={({ field }) => (
render={({ field }) => {
const { value, ...fieldProps } = field
return (
<FormItem>
<FormLabel>{t("MaximumRetryAttempts")}</FormLabel>
<FormControl>
<Input type="number" placeholder="3" {...field} />
<Input
type="number"
placeholder="3"
value={String(value ?? "")}
{...fieldProps}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
)
}}
/>
<FormField
control={form.control}
@@ -351,7 +364,7 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
<FormControl>
<div className="flex items-center gap-2">
<Checkbox
checked={field.value}
checked={field.value === true}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">
@@ -371,7 +384,7 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
<FormControl>
<div className="flex items-center gap-2">
<Checkbox
checked={field.value}
checked={field.value === true}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">
+106 -88
View File
@@ -25,6 +25,7 @@ import {
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import { useMediaQuery } from "@/hooks/useMediaQuery"
import { virtualizedTableFeatures } from "@/lib/table"
import { copyToClipboard, fm, formatPath, fmWorker as worker } from "@/lib/utils"
import {
FMEntry,
@@ -37,7 +38,7 @@ import {
import { ColumnDef } from "@tanstack/react-table"
import { Row, flexRender } from "@tanstack/react-table"
import { File, Folder } from "lucide-react"
import { HTMLAttributes, JSX, useEffect, useRef, useState } from "react"
import { HTMLAttributes, JSX, useCallback, useEffect, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
@@ -59,6 +60,10 @@ interface FMProps {
wsUrl: string
}
type VirtualizedTableRowProps = HTMLAttributes<HTMLTableRowElement> & {
"data-index"?: number | string
}
const arraysEqual = (a: Uint8Array, b: Uint8Array) => {
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) {
@@ -67,21 +72,20 @@ const arraysEqual = (a: Uint8Array, b: Uint8Array) => {
return true
}
const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl, ...props }) => {
export const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({
wsUrl,
...props
}) => {
const { t } = useTranslation()
const fmRef = useRef<HTMLDivElement>(null)
const wsRef = useRef<WebSocket | null>(null)
useEffect(() => {
return () => {
wsRef.current?.close()
}
}, [])
const tRef = useRef(t)
tRef.current = t
const [dOpen, setdOpen] = useState(false)
const [uOpen, setuOpen] = useState(false)
const columns: ColumnDef<FMEntry>[] = [
const columns: ColumnDef<typeof virtualizedTableFeatures, FMEntry>[] = [
{
id: "type",
header: () => <span>{t("Type")}</span>,
@@ -119,10 +123,9 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
},
]
const tableRowComponent = (rows: Row<FMEntry>[]) =>
function getTableRow(props: HTMLAttributes<HTMLTableRowElement>) {
// @ts-expect-error data-index is a valid attribute
const index = props["data-index"]
const tableRowComponent = (rows: Row<typeof virtualizedTableFeatures, FMEntry>[]) =>
function getTableRow(props: VirtualizedTableRowProps) {
const index = Number(props["data-index"])
const row = rows[index]
if (!row) return null
@@ -160,71 +163,6 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
}
}
useEffect(() => {
const url = new URL(wsUrl, window.location.origin)
url.protocol = url.protocol.replace("http", "ws")
const ws = new WebSocket(url)
wsRef.current = ws
ws.binaryType = "arraybuffer"
ws.onopen = () => {
listFile()
}
ws.onclose = (e) => {
console.log("WebSocket connection closed:", e)
}
ws.onerror = (e) => {
console.error(e)
toast("Websocket" + " " + t("Error"), {
description: t("Results.UnExpectedError"),
})
}
ws.onmessage = async (e: MessageEvent<ArrayBufferLike>) => {
try {
const identifier = new Uint8Array(e.data, 0, 4)
if (arraysEqual(identifier, FMIdentifier.error)) {
const errBytes = e.data.slice(4)
const errMsg = new TextDecoder("utf-8").decode(errBytes)
throw new Error(errMsg)
}
if (firstChunk.current) {
if (arraysEqual(identifier, FMIdentifier.file)) {
worker.postMessage({
operation: 1,
arrayBuffer: e.data,
fileName: currentBasename.current,
})
firstChunk.current = false
} else if (arraysEqual(identifier, FMIdentifier.fileName)) {
const { path, fmList } = await fm.parseFMList(e.data)
setPath(path)
setFMEntries(fmList)
} else if (arraysEqual(identifier, FMIdentifier.complete)) {
// Upload completed
setuOpen(false)
listFile()
} else {
throw new Error(t("Results.UnknownIdentifier"))
}
} else {
await waitForHandleReady()
worker.postMessage({
operation: 2,
arrayBuffer: e.data,
fileName: currentBasename.current,
})
}
} catch (error) {
console.error("Error processing received data:", error)
toast("FM" + " " + t("Error"), {
description: t("Results.UnExpectedError"),
})
setdOpen(false)
setuOpen(false)
}
}
}, [wsUrl])
useEffect(() => {
worker.onmessage = async (event: MessageEvent<FMWorkerData>) => {
switch (event.data.type) {
@@ -265,25 +203,101 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload)
}
}, [worker, dOpen])
}, [dOpen])
const [currentPath, setPath] = useState("")
useEffect(() => {
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
listFile()
}
}, [wsRef.current, currentPath])
const currentPathRef = useRef(currentPath)
currentPathRef.current = currentPath
const listFile = () => {
const listFile = useCallback(() => {
const prefix = new Int8Array([FMOpcode.List])
const pathMsg = new TextEncoder().encode(currentPath)
const pathMsg = new TextEncoder().encode(currentPathRef.current)
const msg = new Int8Array(prefix.length + pathMsg.length)
msg.set(prefix)
msg.set(pathMsg, prefix.length)
wsRef.current?.send(msg)
}, [])
// The WebSocket initialization must not depend on listFile or currentPath,
// otherwise navigating directories triggers a disconnect and reconnect.
useEffect(() => {
const url = new URL(wsUrl, window.location.origin)
url.protocol = url.protocol.replace("http", "ws")
const ws = new WebSocket(url)
wsRef.current = ws
ws.binaryType = "arraybuffer"
ws.onopen = () => {
listFile()
}
ws.onclose = (e) => {
console.log("WebSocket connection closed:", e)
}
ws.onerror = (e) => {
console.error(e)
toast("Websocket" + " " + tRef.current("Error"), {
description: tRef.current("Results.UnExpectedError"),
})
}
ws.onmessage = async (e: MessageEvent<ArrayBufferLike>) => {
try {
const identifier = new Uint8Array(e.data, 0, 4)
if (arraysEqual(identifier, FMIdentifier.error)) {
const errBytes = e.data.slice(4)
const errMsg = new TextDecoder("utf-8").decode(errBytes)
throw new Error(errMsg)
}
if (firstChunk.current) {
if (arraysEqual(identifier, FMIdentifier.file)) {
worker.postMessage({
operation: 1,
arrayBuffer: e.data,
fileName: currentBasename.current,
})
firstChunk.current = false
} else if (arraysEqual(identifier, FMIdentifier.fileName)) {
const { path, fmList } = await fm.parseFMList(e.data)
setPath(path)
setFMEntries(fmList)
} else if (arraysEqual(identifier, FMIdentifier.complete)) {
// Upload completed
setuOpen(false)
listFile()
} else {
throw new Error(tRef.current("Results.UnknownIdentifier"))
}
} else {
await waitForHandleReady()
worker.postMessage({
operation: 2,
arrayBuffer: e.data,
fileName: currentBasename.current,
})
}
} catch (error) {
console.error("Error processing received data:", error)
toast("FM" + " " + tRef.current("Error"), {
description: tRef.current("Results.UnExpectedError"),
})
setdOpen(false)
setuOpen(false)
}
}
return () => {
ws.close()
if (wsRef.current === ws) {
wsRef.current = null
}
}
}, [listFile, wsUrl])
useEffect(() => {
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
listFile()
}
}, [currentPath, listFile])
const downloadFile = (basename: string) => {
currentBasename.current = basename
@@ -331,9 +345,13 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
onClick={async () => {
try {
await copyToClipboard(formatPath(currentPath))
} catch (error: any) {
} catch (error) {
const description =
error instanceof Error
? error.message
: t("Results.UnExpectedError")
toast("FM" + " " + t("Error"), {
description: error.message,
description,
})
console.error("copy error: ", error)
}
+16 -2
View File
@@ -51,6 +51,7 @@ const pages = [
{ href: "/dashboard/nat", label: i18next.t("NATT") },
{ href: "/dashboard/domain", label: i18next.t("Domain") }, // <-- 新增的域名监控链接
{ href: "/dashboard/server-group", label: i18next.t("Group") },
{ href: "/dashboard/transfer", label: i18next.t("Transfer.Title") },
]
// ^^^^^^^^^^^ 1. 在这里为移动端菜单添加新页面 ^^^^^^^^^^^
// =======================================================
@@ -59,6 +60,7 @@ export default function Header() {
const { t } = useTranslation()
const { logout } = useAuth()
const profile = useMainStore((store) => store.profile)
const isAdmin = profile?.role === 0
const location = useLocation()
const isDesktop = useMediaQuery("(min-width: 890px)")
@@ -146,6 +148,7 @@ export default function Header() {
{t("Profile")}
</div>
</DropdownMenuItem>
{isAdmin && (
<DropdownMenuItem
onClick={() => {
setDropdownOpen(false)
@@ -158,6 +161,7 @@ export default function Header() {
{t("Settings")}
</div>
</DropdownMenuItem>
)}
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem
@@ -264,6 +268,15 @@ export default function Header() {
<Link to="/dashboard/server-group">{t("Group")}</Link>
</NzNavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NzNavigationMenuLink
asChild
active={location.pathname === "/dashboard/transfer"}
className={navigationMenuTriggerStyle()}
>
<Link to="/dashboard/transfer">{t("Transfer.Title")}</Link>
</NzNavigationMenuLink>
</NavigationMenuItem>
</>
)}
</div>
@@ -353,6 +366,7 @@ export default function Header() {
{t("Profile")}
</div>
</DropdownMenuItem>
{isAdmin && (
<DropdownMenuItem
onClick={() => {
setDropdownOpen(false)
@@ -365,6 +379,7 @@ export default function Header() {
{t("Settings")}
</div>
</DropdownMenuItem>
)}
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={logout} className="cursor-pointer">
@@ -399,8 +414,7 @@ const useInterval = (callback: () => void, delay?: number | null) => {
function Overview() {
const { t } = useTranslation()
const profile = useMainStore((store) => store.profile)
const timeOption = DateTime.TIME_SIMPLE
timeOption.hour12 = true
const timeOption = { ...DateTime.TIME_SIMPLE, hour12: true }
const [timeString, setTimeString] = useState(
DateTime.now().setLocale("en-US").toLocaleString(timeOption),
)
+5 -4
View File
@@ -147,11 +147,11 @@ const generateCommand = (
const env = envParts.join(" ")
const envWinParts = [
`$env:NZ_SERVER=\"${install_host}\";`,
`$env:NZ_TLS=\"${tls || false}\";`,
`$env:NZ_CLIENT_SECRET=\"${agent_secret}\";`,
`$env:NZ_SERVER="${install_host}";`,
`$env:NZ_TLS="${tls || false}";`,
`$env:NZ_CLIENT_SECRET="${agent_secret}";`,
]
if (uuid) envWinParts.push(`$env:NZ_UUID=\"${uuid}\";`)
if (uuid) envWinParts.push(`$env:NZ_UUID="${uuid}";`)
const env_win = envWinParts.join("")
switch (type) {
@@ -161,6 +161,7 @@ const generateCommand = (
}
case OSTypes.Windows: {
return `${env_win} [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Ssl3 -bor [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12;set-ExecutionPolicy RemoteSigned;Invoke-WebRequest ${window.location.origin}/script/agent.ps1 -OutFile C:\\install.ps1;powershell.exe C:\\install.ps1`
}
default: {
throw new Error(`Unknown OS: ${type}`)
+25 -11
View File
@@ -46,16 +46,17 @@ const natFormSchema = z.object({
domain: z.string(),
})
type NatFormData = z.infer<typeof natFormSchema>
type NatFormInput = z.input<typeof natFormSchema>
type NatFormData = z.output<typeof natFormSchema>
export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
const { t } = useTranslation()
const form = useForm<NatFormData>({
resolver: zodResolver(natFormSchema as any),
const form = useForm<NatFormInput, unknown, NatFormData>({
resolver: zodResolver(natFormSchema),
defaultValues: data
? {
name: data.name ?? "",
enabled: (data as any).enabled ?? false,
enabled: data.enabled ?? false,
server_id: data.server_id ?? 0,
host: data.host ?? "",
domain: data.domain ?? "",
@@ -76,11 +77,16 @@ export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
const onSubmit = async (values: NatFormData) => {
try {
data?.id ? await updateNAT(data.id, values) : await createNAT(values)
} catch (e) {
if (data?.id) {
await updateNAT(data.id, values)
} else {
await createNAT(values)
}
} catch (e: unknown) {
console.error(e)
toast(t("Error"), {
description: t("Results.UnExpectedError"),
description:
e instanceof Error && e.message ? e.message : t("Results.UnExpectedError"),
})
return
}
@@ -119,15 +125,23 @@ export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
<FormField
control={form.control}
name="server_id"
render={({ field }) => (
render={({ field }) => {
const { value, ...fieldProps } = field
return (
<FormItem>
<FormLabel>{t("Server")} ID</FormLabel>
<FormControl>
<Input type="number" placeholder="1" {...field} />
<Input
type="number"
placeholder="1"
value={String(value ?? "")}
{...fieldProps}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
)
}}
/>
<FormField
control={form.control}
@@ -169,7 +183,7 @@ export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
<FormControl>
<div className="flex items-center gap-2">
<Checkbox
checked={field.value}
checked={field.value === true}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">{t("Enable")}</Label>
+5 -3
View File
@@ -64,9 +64,11 @@ export const NotificationGroupCard: React.FC<NotificationGroupCardProps> = ({ da
const onSubmit = async (values: z.infer<typeof notificationGroupFormSchema>) => {
try {
data?.group.id
? await updateNotificationGroup(data.group.id, values)
: await createNotificationGroup(values)
if (data?.group.id) {
await updateNotificationGroup(data.group.id, values)
} else {
await createNotificationGroup(values)
}
} catch (e) {
console.error(e)
toast(t("Error"), {
+19 -11
View File
@@ -62,10 +62,14 @@ const notificationFormSchema = z.object({
export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
const { t } = useTranslation()
type notificationFormData = z.infer<typeof notificationFormSchema>
type NotificationFormInput = z.input<typeof notificationFormSchema>
type NotificationFormData = z.output<typeof notificationFormSchema>
type NotificationDefaults = ModelNotification &
Partial<Pick<NotificationFormData, "skip_check">>
const notificationDefaults: NotificationDefaults | undefined = data
const form = useForm({
resolver: zodResolver(notificationFormSchema) as any,
const form = useForm<NotificationFormInput, unknown, NotificationFormData>({
resolver: zodResolver(notificationFormSchema),
defaultValues: data
? {
name: data.name ?? "",
@@ -75,10 +79,12 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
request_header: data.request_header ?? "",
request_body: data.request_body ?? "",
verify_tls: data.verify_tls ?? false,
skip_check: data.skip_check ?? false,
skip_check: notificationDefaults?.skip_check ?? false,
format_metric_units: data.format_metric_units ?? false,
type: data.type ?? 1,
type: (data as any)?.type ?? 1,
}
: {
name: "",
url: "",
@@ -98,9 +104,13 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
const [open, setOpen] = useState(false)
const onSubmit = async (values: notificationFormData) => {
const onSubmit = async (values: NotificationFormData) => {
try {
data?.id ? await updateNotification(data.id, values) : await createNotification(values)
if (data?.id) {
await updateNotification(data.id, values)
} else {
await createNotification(values)
}
} catch (e) {
console.error(e)
toast(t("Error"), {
@@ -128,10 +138,7 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit as any)}
className="space-y-2 my-2"
>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 my-2">
<FormField
control={form.control}
name="name"
@@ -343,6 +350,7 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
/>
</div>
</div>
<DialogFooter className="justify-end">
<DialogClose asChild>
<Button type="button" className="my-2" variant="secondary">
+3 -3
View File
@@ -49,7 +49,7 @@ const agentConfigSchema = z.object({
try {
JSON.parse(val)
return true
} catch (e) {
} catch {
return false
}
},
@@ -66,7 +66,7 @@ const agentConfigSchema = z.object({
try {
JSON.parse(val)
return true
} catch (e) {
} catch {
return false
}
},
@@ -129,7 +129,7 @@ export const ServerConfigCard = ({ sid, menuItem = false, ...props }: ServerConf
}
}
if (open) fetchData()
}, [open])
}, [open, sid, t])
const form = useForm({
resolver: zodResolver(agentConfigSchema) as any,
+5 -3
View File
@@ -64,9 +64,11 @@ export const ServerGroupCard: React.FC<ServerGroupCardProps> = ({ data, mutate }
const onSubmit = async (values: z.infer<typeof serverGroupFormSchema>) => {
try {
data?.group.id
? await updateServerGroup(data.group.id, values)
: await createServerGroup(values)
if (data?.group.id) {
await updateServerGroup(data.group.id, values)
} else {
await createServerGroup(values)
}
} catch (e) {
console.error(e)
toast(t("Error"), {
+6 -3
View File
@@ -28,7 +28,8 @@ import { asOptionalField } from "@/lib/utils"
import { ModelServer } from "@/types"
import { zodResolver } from "@hookform/resolvers/zod"
import { useEffect, useState } from "react"
import { useForm } from "react-hook-form"
import { useForm, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { KeyedMutator } from "swr"
@@ -55,7 +56,7 @@ const serverFormSchema = z.object({
try {
JSON.parse(val)
return true
} catch (e) {
} catch {
return false
}
},
@@ -88,6 +89,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
keepDefaultValues: false,
},
})
const enableDDNS = useWatch({ control: form.control, name: "enable_ddns" })
const [open, setOpen] = useState(false)
@@ -189,7 +191,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
</FormItem>
)}
/>
{form.watch("enable_ddns") ? (
{enableDDNS ? (
<>
<FormField
control={form.control as any}
@@ -331,6 +333,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
</FormItem>
)}
/>
<DialogFooter className="justify-end">
<DialogClose asChild>
<Button type="button" className="my-2" variant="secondary">
+10 -7
View File
@@ -56,8 +56,8 @@ const serviceFormSchema = z.object({
cover: z.coerce.number().int().min(0),
display_index: z.coerce.number().int(),
duration: z.coerce.number().int().min(30),
enable_show_in_service: asOptionalField(z.boolean()),
enable_trigger_task: asOptionalField(z.boolean()),
hide_for_guest: asOptionalField(z.boolean()),
fail_trigger_tasks: z.array(z.number()),
fail_trigger_tasks_raw: z.string(),
latency_notify: asOptionalField(z.boolean()),
@@ -113,11 +113,14 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
values.skip_servers = conv.arrToRecord(values.skip_servers_raw)
values.fail_trigger_tasks = conv.strToArr(values.fail_trigger_tasks_raw).map(Number)
values.recover_trigger_tasks = conv.strToArr(values.recover_trigger_tasks_raw).map(Number)
const { skip_servers_raw, ...requiredFields } = values
const requiredFields = { ...values }
delete (requiredFields as Record<string, unknown>).skip_servers_raw
try {
data?.id
? await updateService(data.id, requiredFields)
: await createService(requiredFields)
if (data?.id) {
await updateService(data.id, requiredFields)
} else {
await createService(requiredFields)
}
} catch (e) {
console.error(e)
toast(t("Error"), {
@@ -232,7 +235,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
/>
<FormField
control={form.control}
name="enable_show_in_service"
name="hide_for_guest"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
@@ -242,7 +245,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
onCheckedChange={field.onChange}
/>
<Label className="text-sm">
{t("ShowInService")}
{t("HideForGuest")}
</Label>
</div>
</FormControl>
+7 -3
View File
@@ -8,10 +8,11 @@ export const SettingsTab = ({ className }: { className?: string }) => {
const { profile } = useAuth()
const isAdmin = profile?.role === 0
const colsClass = isAdmin ? "grid-cols-5" : "grid-cols-1"
return (
<Tabs defaultValue={window.location.pathname} className={className}>
<TabsList className="grid w-full grid-cols-4">
<TabsList className={`grid w-full ${colsClass}`}>
{isAdmin && (
<>
<TabsTrigger value="/dashboard/settings" asChild>
@@ -20,14 +21,17 @@ export const SettingsTab = ({ className }: { className?: string }) => {
<TabsTrigger value="/dashboard/settings/user" asChild>
<Link to="/dashboard/settings/user">{t("User")}</Link>
</TabsTrigger>
</>
)}
<TabsTrigger value="/dashboard/settings/online-user" asChild>
<Link to="/dashboard/settings/online-user">{t("OnlineUser")}</Link>
</TabsTrigger>
<TabsTrigger value="/dashboard/settings/waf" asChild>
<Link to="/dashboard/settings/waf">{t("WAF")}</Link>
</TabsTrigger>
</>
)}
<TabsTrigger value="/dashboard/settings/api-tokens" asChild>
<Link to="/dashboard/settings/api-tokens">{t("ApiTokens")}</Link>
</TabsTrigger>
</TabsList>
</Tabs>
)
+1 -1
View File
@@ -70,7 +70,7 @@ function Calendar({
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
defaultClassNames.caption_label,
),
table: "w-full border-collapse",
month_grid: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
+113 -3
View File
@@ -1,7 +1,117 @@
export function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) {
import { useEffect, useState } from "react"
type SVGProps = React.ComponentPropsWithoutRef<"svg">
type IconMarkupState = { status: "ready"; icon: ParsedIconMarkup } | { status: "missing" }
type ParsedIconMarkup = {
title?: string
viewBox: string
path: string
}
const simpleIconModules = import.meta.glob("/node_modules/simple-icons/icons/*.svg", {
query: "?raw",
import: "default",
})
const iconLoadersBySlug = Object.fromEntries(
Object.entries(simpleIconModules).map(([path, loader]) => [
path.slice(path.lastIndexOf("/") + 1, -".svg".length),
loader as () => Promise<string>,
]),
)
const iconMarkupCache = new Map<string, IconMarkupState>()
function toProviderSlug(provider: string) {
return provider
.trim()
.replace(/[^a-z0-9]+/gi, "")
.toLowerCase()
}
function loadProviderIconMarkup(provider: string) {
const loader = iconLoadersBySlug[toProviderSlug(provider)]
return loader ? loader() : null
}
function parseIconMarkup(markup: string): ParsedIconMarkup | null {
const viewBox = markup.match(/viewBox="([^"]+)"/i)?.[1] ?? "0 0 24 24"
const title = markup.match(/<title>([^<]*)<\/title>/i)?.[1]
const path = markup.match(/<path d="([^"]+)"/i)?.[1]
if (!path) {
return null
}
return { title, viewBox, path }
}
async function loadProviderIcon(provider: string): Promise<IconMarkupState> {
const markup = await loadProviderIconMarkup(provider)
if (!markup) {
return { status: "missing" }
}
const parsed = parseIconMarkup(markup)
if (!parsed) {
return { status: "missing" }
}
return { status: "ready", icon: parsed }
}
export function OAuthProviderIcon({
provider,
title,
...props
}: SVGProps & { provider: string; title?: string }) {
const providerSlug = toProviderSlug(provider)
const [iconState, setIconState] = useState<IconMarkupState | null>(() => {
return iconMarkupCache.get(providerSlug) ?? null
})
useEffect(() => {
if (iconMarkupCache.has(providerSlug)) {
setIconState(iconMarkupCache.get(providerSlug) ?? null)
return
}
let cancelled = false
loadProviderIcon(provider)
.then((result) => {
if (cancelled) return
iconMarkupCache.set(providerSlug, result)
setIconState(result)
})
.catch(() => {
if (cancelled) return
const missingState = { status: "missing" } as const
iconMarkupCache.set(providerSlug, missingState)
setIconState(missingState)
})
return () => {
cancelled = true
}
}, [provider, providerSlug])
if (!iconState || iconState.status !== "ready") {
return null
}
const iconTitle = title ?? iconState.icon.title
return (
<svg viewBox="0 0 496 512" fill="currentColor" {...props}>
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" />
<svg
viewBox={iconState.icon.viewBox}
fill="currentColor"
aria-hidden={iconTitle ? undefined : "true"}
role={iconTitle ? "img" : undefined}
{...props}
>
{iconTitle ? <title>{iconTitle}</title> : null}
<path d={iconState.icon.path} />
</svg>
)
}
+1 -1
View File
@@ -1,7 +1,7 @@
import { cn } from "@/lib/utils"
import { InputHTMLAttributes, forwardRef } from "react"
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {}
export type InputProps = InputHTMLAttributes<HTMLInputElement>
const Input = forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
return (
-1
View File
@@ -141,7 +141,6 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
animation = 0,
maxCount = 3,
modalPopover = false,
asChild = false,
className,
...props
},
+13 -12
View File
@@ -3,16 +3,16 @@
import { ScrollArea } from "@/components/ui/scroll-area"
import { TableCell, TableHead, TableRow } from "@/components/ui/table"
import { useMediaQuery } from "@/hooks/useMediaQuery"
import { virtualizedTableFeatures } from "@/lib/table"
import { cn } from "@/lib/utils"
import {
ColumnDef,
Row,
RowData,
SortDirection,
SortingState,
flexRender,
getCoreRowModel,
getSortedRowModel,
useReactTable,
useTable,
} from "@tanstack/react-table"
import { HTMLAttributes, JSX, forwardRef, useEffect, useRef, useState } from "react"
import { TableVirtuoso } from "react-virtuoso"
@@ -26,7 +26,9 @@ const TableComponent = forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTab
)
TableComponent.displayName = "TableComponent"
const TableRowComponent = <TData,>(rows: Row<TData>[]) =>
const TableRowComponent = <TData extends RowData>(
rows: Row<typeof virtualizedTableFeatures, TData>[],
) =>
function getTableRow(props: HTMLAttributes<HTMLTableRowElement>) {
// @ts-expect-error data-index is a valid attribute
const index = props["data-index"]
@@ -64,34 +66,33 @@ function SortingIndicator({ isSorted }: { isSorted: SortDirection | false }) {
)
}
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
interface DataTableProps<TData extends RowData> {
columns: ColumnDef<typeof virtualizedTableFeatures, TData>[]
data: TData[]
rowComponent?: (
rows: Row<TData>[],
rows: Row<typeof virtualizedTableFeatures, TData>[],
) => (props: HTMLAttributes<HTMLTableRowElement>) => JSX.Element | null
}
export function DataTable<TData, TValue>({
export function DataTable<TData extends RowData>({
columns,
data,
rowComponent,
}: DataTableProps<TData, TValue>) {
}: DataTableProps<TData>) {
const [sorting, setSorting] = useState<SortingState>([
{
id: "type",
desc: true,
},
])
const table = useReactTable({
const table = useTable({
features: virtualizedTableFeatures,
data,
columns,
state: {
sorting,
},
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
})
const { rows } = table.getRowModel()
+46 -13
View File
@@ -1,6 +1,6 @@
import { getProfile, login as loginRequest } from "@/api/user"
import { AuthContextProps } from "@/types"
import { createContext, useContext, useEffect, useMemo } from "react"
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import { toast } from "sonner"
@@ -9,35 +9,57 @@ import { useMainStore } from "./useMainStore"
const AuthContext = createContext<AuthContextProps>({
profile: undefined,
loading: true,
login: () => {},
loginOauth2: () => {},
logout: () => {},
})
// Admin is role 0 on the backend. A missing/non-numeric role must never
// collapse to 0, or a malformed profile response would be treated as admin
// client-side; default unknown roles to a non-admin value instead.
const NON_ADMIN_ROLE = 1
function normalizeRole(role: unknown): number {
return typeof role === "number" && Number.isFinite(role) ? role : NON_ADMIN_ROLE
}
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const profile = useMainStore((store) => store.profile)
const setProfile = useMainStore((store) => store.setProfile)
const [loading, setLoading] = useState(true)
const { t } = useTranslation()
// An explicit login/logout (or its getProfile) resolving while the initial
// mount probe is still in flight must win: bump this so the stale probe's
// result is discarded instead of clobbering the authenticated state.
const authEpoch = useRef(0)
useEffect(() => {
const epoch = authEpoch.current
;(async () => {
try {
const user = await getProfile()
user.role = user.role || 0
if (authEpoch.current !== epoch) return
user.role = normalizeRole(user.role)
setProfile(user)
} catch (error: any) {
} catch {
if (authEpoch.current !== epoch) return
setProfile(undefined)
} finally {
setLoading(false)
}
})()
}, [])
}, [setProfile])
const navigate = useNavigate()
const login = async (username: string, password: string) => {
const login = useCallback(
async (username: string, password: string) => {
try {
await loginRequest(username, password)
const user = await getProfile()
user.role = user.role || 0
authEpoch.current++
user.role = normalizeRole(user.role)
setProfile(user)
navigate("/dashboard")
} catch (error: any) {
@@ -47,40 +69,51 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
} else {
toast(msg || t("NetworkError"))
}
} finally {
// An explicit login resolves auth regardless of the still-pending
// mount probe; clear loading so ProtectedRoute stops blanking.
setLoading(false)
}
}
},
[navigate, setProfile, t],
)
const loginOauth2 = async () => {
const loginOauth2 = useCallback(async () => {
try {
const user = await getProfile()
user.role = user.role || 0
authEpoch.current++
user.role = normalizeRole(user.role)
setProfile(user)
navigate("/dashboard")
} catch (error: any) {
toast(error.message)
} finally {
setLoading(false)
window.history.replaceState({}, document.title, window.location.pathname)
}
}
}, [navigate, setProfile])
const logout = () => {
const logout = useCallback(() => {
authEpoch.current++
document.cookie.split(";").forEach(function (c) {
document.cookie = c
.replace(/^ +/, "")
.replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/")
})
setProfile(undefined)
setLoading(false)
navigate("/dashboard/login", { replace: true })
}
}, [navigate, setProfile])
const value = useMemo(
() => ({
profile,
loading,
login,
loginOauth2,
logout,
}),
[profile],
[profile, loading, login, loginOauth2, logout],
)
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}
+10 -13
View File
@@ -1,19 +1,16 @@
import { useEffect, useState } from "react"
import { useCallback, useSyncExternalStore } from "react"
export function useMediaQuery(query: string) {
const [value, setValue] = useState(false)
useEffect(() => {
function onChange(event: MediaQueryListEvent) {
setValue(event.matches)
}
const subscribe = useCallback(
(callback: () => void) => {
const result = matchMedia(query)
result.addEventListener("change", onChange)
setValue(result.matches)
result.addEventListener("change", callback)
return () => result.removeEventListener("change", callback)
},
[query],
)
return () => result.removeEventListener("change", onChange)
}, [query])
const getSnapshot = useCallback(() => matchMedia(query).matches, [query])
return value
return useSyncExternalStore(subscribe, getSnapshot, () => false)
}
+1 -1
View File
@@ -54,7 +54,7 @@ export const NotificationProvider: React.FC<NotificationProviderProps> = ({
setNotifier(undefined)
}
})()
}, [location.pathname])
}, [location.pathname, setNotifier, setNotifierGroup, withNotifier, withNotifierGroup])
const value: NotificationContextProps = useMemo(
() => ({
+1 -1
View File
@@ -54,7 +54,7 @@ export const ServerProvider: React.FC<ServerProviderProps> = ({
setServer(undefined)
}
})()
}, [location.pathname])
}, [location.pathname, setServer, setServerGroup, withServer, withServerGroup])
const value: ServerContextProps = useMemo(
() => ({
+155 -6
View File
@@ -1,8 +1,42 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";
@plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--color-chart-1: hsl(var(--chart-1));
--color-chart-2: hsl(var(--chart-2));
--color-chart-3: hsl(var(--chart-3));
--color-chart-4: hsl(var(--chart-4));
--color-chart-5: hsl(var(--chart-5));
--text-xsm: 0.825rem;
--text-xsm--line-height: 1.25rem;
}
@layer base {
:root {
--radius: 0.5rem;
--background: 0 0% 100%;
@@ -57,7 +91,6 @@
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@@ -84,5 +117,121 @@ body,
}
::-webkit-scrollbar-thumb {
@apply rounded-full border-[1px] border-solid border-transparent bg-border bg-clip-padding;
@apply bg-border rounded-full border-[1px] border-solid border-transparent bg-clip-padding;
}
.terminal-shell {
--terminal-available-height: 70vh;
display: flex;
height: min(70vh, var(--terminal-available-height));
min-height: 360px;
flex-direction: column;
overflow: hidden;
border: 1px solid hsl(var(--border));
border-radius: var(--radius);
background: #09090b;
}
.terminal-screen {
min-height: 0;
flex: 1 1 auto;
padding: 6px 3px 2px 6px;
touch-action: pan-y;
}
.terminal-screen .xterm {
height: 100%;
}
.terminal-screen .xterm-viewport {
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
}
.terminal-keyboard {
display: flex;
flex: 0 0 auto;
gap: 4px;
overflow-x: auto;
padding: 6px 6px calc(6px + env(safe-area-inset-bottom));
border-top: 1px solid hsl(var(--border));
background: hsl(var(--background));
touch-action: pan-x;
-webkit-overflow-scrolling: touch;
}
.terminal-key {
display: inline-flex;
min-width: 46px;
height: 42px;
flex: 0 0 auto;
align-items: center;
justify-content: center;
border: 1px solid hsl(var(--border));
border-radius: calc(var(--radius) - 2px);
background: hsl(var(--secondary));
padding: 0 9px;
color: hsl(var(--secondary-foreground));
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.75rem;
font-weight: 500;
user-select: none;
-webkit-user-select: none;
}
.terminal-key:hover:not(:disabled),
.terminal-key:focus-visible,
.terminal-key.is-active {
border-color: hsl(var(--ring));
background: hsl(var(--accent));
outline: none;
}
.terminal-key:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.terminal-key-arrow {
min-width: 40px;
padding-inline: 5px;
}
.terminal-key-paste {
min-width: 58px;
}
.terminal-shell:fullscreen {
width: 100%;
height: 100%;
min-height: 0;
border: 0;
border-radius: 0;
}
@media (max-width: 640px) {
.terminal-shell {
height: var(--terminal-available-height);
min-height: 220px;
}
.terminal-keyboard {
gap: 2px;
padding-inline: 4px;
}
.terminal-key {
min-width: 43px;
height: 44px;
padding-inline: 5px;
}
.terminal-key-arrow {
min-width: 36px;
padding-inline: 2px;
}
.terminal-key-paste {
min-width: 49px;
}
}
+32 -2
View File
@@ -5,10 +5,14 @@ import deTranslation from "../locales/de/translation.json"
import enTranslation from "../locales/en/translation.json"
import esTranslation from "../locales/es/translation.json"
import frTranslation from "../locales/fr/translation.json"
import glTranslation from "../locales/gl/translation.json"
import idTranslation from "../locales/id/translation.json"
import itTranslation from "../locales/it/translation.json"
import jaTranslation from "../locales/ja/translation.json"
import roTranslation from "../locales/ro/translation.json"
import ruTranslation from "../locales/ru/translation.json"
import taTranslation from "../locales/ta/translation.json"
import ukTranslation from "../locales/uk/translation.json"
import zhCNTranslation from "../locales/zh-CN/translation.json"
import zhTWTranslation from "../locales/zh-TW/translation.json"
@@ -43,10 +47,29 @@ const resources = {
"id-ID": {
translation: idTranslation,
},
"ja-JP": {
translation: jaTranslation,
},
"ro-RO": {
translation: roTranslation,
},
"uk-UA": {
translation: ukTranslation,
},
"gl-ES": {
translation: glTranslation,
},
}
const getStoredLanguage = () => {
return localStorage.getItem("language") || "en-US"
try {
if (typeof window !== "undefined" && window.localStorage) {
return window.localStorage.getItem("language") || "en-US"
}
} catch {
// ignore
}
return "en-US"
}
i18n.use(initReactI18next).init({
@@ -59,7 +82,14 @@ i18n.use(initReactI18next).init({
})
i18n.on("languageChanged", (lng) => {
localStorage.setItem("language", lng)
try {
if (typeof window !== "undefined" && window.localStorage) {
window.localStorage.setItem("language", lng)
}
} catch {
// ignore
}
})
export default i18n
+29
View File
@@ -0,0 +1,29 @@
import {
columnSizingFeature,
columnVisibilityFeature,
createSortedRowModel,
rowSelectionFeature,
rowSortingFeature,
sortFn_alphanumeric,
sortFn_datetime,
sortFn_text,
tableFeatures,
} from "@tanstack/react-table"
export const selectableTableFeatures = tableFeatures({
columnVisibilityFeature,
rowSelectionFeature,
})
export const virtualizedTableFeatures = tableFeatures({
columnSizingFeature,
columnVisibilityFeature,
rowSelectionFeature,
rowSortingFeature,
sortedRowModel: createSortedRowModel(),
sortFns: {
alphanumeric: sortFn_alphanumeric,
datetime: sortFn_datetime,
text: sortFn_text,
},
})
+13
View File
@@ -133,6 +133,19 @@ export function formatPath(path: string) {
return path.replace(/\/{2,}/g, "/")
}
// Returns the URL only if it uses an http(s) scheme, else undefined. Guards
// against rendering attacker-controlled template metadata as a clickable
// javascript:/data: href.
export function safeExternalHref(url?: string): string | undefined {
if (!url) return undefined
try {
const parsed = new URL(url, window.location.origin)
return parsed.protocol === "https:" || parsed.protocol === "http:" ? parsed.href : undefined
} catch {
return undefined
}
}
export function joinIP(p?: ModelIP) {
if (p) {
if (p.ipv4_addr && p.ipv6_addr) {
+30 -2
View File
@@ -148,6 +148,10 @@
"Loading": "Laden",
"Services": "Services",
"DashboardOriginalHost": "Agent Verbindungsadresse [Domainname/IP:port]",
"DashboardHost": "Dashboard-Host für OAuth2-Callback [Domainname/IP:port]",
"DashboardHostHint": "Der öffentliche Host des Dashboards zum Erstellen der OAuth2-Callback-URL. Setzen Sie diesen Wert, wenn das Dashboard über eine andere Domain als die Agent-Verbindungsadresse erreichbar ist. Leer lassen, um dem Request-Host unverändert zu vertrauen.",
"ReservedHosts": "Reservierte Hosts (kommagetrennt; öffentliche/Reverse-Proxy-Domains, die Mitglieder nicht als NAT-Domain nutzen dürfen)",
"ReservedHostsHint": "Hinter einem Reverse-Proxy sieht das Dashboard seine eigene öffentliche Domain nicht und ist daher nicht automatisch geschützt. Tragen Sie hier jeden öffentlichen Eingangs-Hostnamen ein, sonst könnte ein Mitglied ihn als NAT-Domain registrieren und das Dashboard-Routing übernehmen.",
"NavigateTo": "Navigieren",
"Offline": "Offline",
"Interval": "Intervall",
@@ -161,7 +165,6 @@
"Login": "Anmelden",
"NewPassword": "Neues Passwort",
"FileManager": "Pseudo File Manager",
"ShowInService": "Zeige in Service",
"CommunityThemeDescription": "Dieses Design wird von der Community bereitgestellt, Benutzung auf eigene Gefahr",
"Language": "Sprache",
"FullIPNotification": "Vollständige IP-Adresse in Benachrichtigung anzeigen",
@@ -180,5 +183,30 @@
"EmptyNote": "Du hast noch keine Notizen.",
"BackToHome": "Zurück zur Startseite",
"OnAlert": "Server mit Warnung",
"EmptyText": "Text ist leer"
"EmptyText": "Text ist leer",
"EnableMCP": "Enable MCP endpoint (off by default; review API token scopes and server allow-list first)",
"ApiTokens": "API Tokens",
"CreateApiToken": "Create API token",
"CreateApiTokenDescription": "Tokens authenticate MCP and external clients on your behalf. They cannot exceed your own permissions.",
"ConfirmDeleteApiToken": "Revoke API token '{{name}}'? This cannot be undone.",
"ApiTokenRevoked": "API token revoked.",
"ApiTokenCreated": "API token created",
"ApiTokenRevealOnce": "Copy this token now. It will never be shown again.",
"ApiTokenStoreSafely": "Treat this token like a password. Anyone with it can act on your behalf within its scopes.",
"ApiTokenServers": "Servers",
"ApiTokenAllServers": "all permitted",
"ApiTokenNever": "never",
"ApiTokenExpiresAt": "Expires",
"ApiTokenLastUsed": "Last used",
"ApiTokenServerIDs": "Restrict to server IDs (optional)",
"ApiTokenServerIDsPlaceholder": "comma-separated, e.g. 1,2,3",
"ApiTokenExpiresInDays": "Expires in (days, 0 = never)",
"ApiTokenScopeRequired": "At least one scope is required.",
"ApiTokenServersInvalid": "Server IDs must be positive integers.",
"ApiTokenExpiryInvalid": "Days must be between 0 and 3650.",
"NameRequired": "Name is required.",
"Revoke": "Revoke",
"Copy": "Copy",
"Copied": "Copied to clipboard",
"Scopes": "Scopes"
}
+64 -2
View File
@@ -83,7 +83,6 @@
"Confirm": "Confirm",
"ConfirmDeletion": "Confirm Deletion?",
"Services": "Services",
"ShowInService": "Show in Service",
"Coverages": {
"Excludes": "Excludes Specific Servers",
"Only": "Only Specific Servers",
@@ -136,9 +135,16 @@
"EditServerGroup": "Edit Server Group",
"CreateServerGroup": "Create Server Group",
"User": "User",
"Owner": "Owner",
"GlobalAgent": "Global Agent",
"UnknownUser": "Unknown user (#{{id}})",
"WAF": "Web Application Firewall",
"SiteName": "Site Name",
"DashboardOriginalHost": "Agent connecting address [domain name/IP:port]",
"DashboardHost": "Dashboard host for OAuth2 callback [domain name/IP:port]",
"DashboardHostHint": "The dashboard's public host used to build the OAuth2 callback URL. Set this when the dashboard is reached on a different domain than the agent connecting address. Leave empty to trust the request Host as-is.",
"ReservedHosts": "Reserved hosts (comma-separated; public/reverse-proxy domains members cannot use as NAT domains)",
"ReservedHostsHint": "Behind a reverse proxy the dashboard cannot see its own public domain, so it is not auto-protected. List every public entry hostname here, or a member could register it as a NAT domain and hijack dashboard routing.",
"ConfigTLS": "Use TLS to connect Agent",
"LoginFailed": "Login Failed",
"BruteForceAttackingToken": "Brute Force Attacking Token",
@@ -157,6 +163,7 @@
"DomainNotificationDays": "Domain Notification Days",
"ServerNotificationDays": "Server Notification Days",
"FullIPNotification": "Show Full IP Address in Notification Messages",
"EnableMCP": "Enable MCP endpoint (off by default; review API token scopes and server allow-list first)",
"EditService": "Edit Service",
"CreateService": "Create Service",
"EditTask": "Edit Task",
@@ -260,5 +267,60 @@
"ClipboardWriteFailed": "Clipboard write failed",
"PastedFromClipboard": "Pasted from clipboard",
"ClipboardReadFailed": "Clipboard read failed",
"FormatMetricUnits": "Format Metric Units"
"FormatMetricUnits": "Format Metric Units",
"Status": "Status",
"ServerID": "Server ID",
"CreatedAt": "Created At",
"BatchMoveServer": "Batch Move Server Owner",
"Servers": "Servers",
"ToUser": "To User",
"Move": "Move",
"Transfer": {
"Title": "Server Transfers",
"PageHint": "Tracks ownership transfers initiated from the batch-move action. Pending entries can be cancelled; terminal entries can be retried.",
"From": "From",
"To": "To",
"Initiator": "Initiator",
"LastError": "Last Error",
"Retry": "Retry",
"CancelRequested": "Cancellation requested",
"RetryRequested": "Retry requested",
"BatchSubmitted": "Batch transfer submitted",
"PendingCount": "{{count}} pending",
"PermissionDeniedCount": "{{count}} permission denied",
"AlreadyTransferringCount": "{{count}} already transferring",
"ServerNotFoundCount": "{{count}} not found",
"SameOwnerCount": "{{count}} already owned by target",
"AgentTooOldCount": "{{count}} agent too old (upgrade required)",
"StatusPending": "Pending",
"StatusVerified": "Verified",
"StatusFailed": "Failed",
"StatusTimeout": "Timeout",
"StatusCancelled": "Cancelled"
},
"ApiTokens": "API Tokens",
"CreateApiToken": "Create API token",
"CreateApiTokenDescription": "Tokens authenticate MCP and external clients on your behalf. They cannot exceed your own permissions.",
"ConfirmDeleteApiToken": "Revoke API token '{{name}}'? This cannot be undone.",
"ApiTokenRevoked": "API token revoked.",
"ApiTokenCreated": "API token created",
"ApiTokenRevealOnce": "Copy this token now. It will never be shown again.",
"ApiTokenStoreSafely": "Treat this token like a password. Anyone with it can act on your behalf within its scopes.",
"ApiTokenServers": "Servers",
"ApiTokenAllServers": "all permitted",
"ApiTokenNever": "never",
"ApiTokenExpiresAt": "Expires",
"ApiTokenLastUsed": "Last used",
"ApiTokenServerIDs": "Restrict to server IDs (optional)",
"ApiTokenServerIDsPlaceholder": "comma-separated, e.g. 1,2,3",
"ApiTokenExpiresInDays": "Expires in (days, 0 = never)",
"ApiTokenScopeRequired": "At least one scope is required.",
"ApiTokenServersInvalid": "Server IDs must be positive integers.",
"ApiTokenExpiryInvalid": "Days must be between 0 and 3650.",
"NameRequired": "Name is required.",
"Revoke": "Revoke",
"Copy": "Copy",
"Copied": "Copied to clipboard",
"Done": "Done",
"Scopes": "Scopes"
}
+29 -2
View File
@@ -51,7 +51,6 @@
"Coverage": "Cobertura",
"LocalService": "Servicio local",
"InstallCommands": "Comando de instalación",
"ShowInService": "Mostrar en servicio",
"Coverages": {
"Alarmed": "Ejecutado en el servidor que activó la alarma",
"Excludes": "Excluir servidores específicos",
@@ -158,6 +157,10 @@
"RequestType": "Tipo de solicitud",
"RequestBody": "Cuerpo de la solicitud",
"DashboardOriginalHost": "Dirección de conexión del Agent [nombre de dominio/IP:puerto]",
"DashboardHost": "Host del panel para el callback de OAuth2 [nombre de dominio/IP:puerto]",
"DashboardHostHint": "Host público del panel usado para construir la URL de callback de OAuth2. Configúralo cuando se acceda al panel en un dominio distinto al de la dirección de conexión del Agent. Déjalo vacío para confiar en el Host de la petición tal cual.",
"ReservedHosts": "Hosts reservados (separados por comas; dominios públicos/proxy inverso que los miembros no pueden usar como dominio NAT)",
"ReservedHostsHint": "Detrás de un proxy inverso, el panel no ve su propio dominio público, por lo que no se protege automáticamente. Indique aquí todos los nombres de host de entrada públicos; de lo contrario, un miembro podría registrarlo como dominio NAT y secuestrar el enrutamiento del panel.",
"ConfigTLS": "Usar TLS para conectar el Agent",
"CustomCodes": "Códigos personalizados (Style y Script)",
"CustomCodesDashboard": "Códigos personalizados para el Dashboard",
@@ -254,5 +257,29 @@
"CopiedToClipboard": "Copiar al portapapeles",
"ClipboardWriteFailed": "Error al escribir en el portapapeles",
"PastedFromClipboard": "Pegado del portapapeles",
"ClipboardReadFailed": "Falló la lectura del portapapeles"
"ClipboardReadFailed": "Falló la lectura del portapapeles",
"EnableMCP": "Enable MCP endpoint (off by default; review API token scopes and server allow-list first)",
"ApiTokens": "API Tokens",
"CreateApiToken": "Create API token",
"CreateApiTokenDescription": "Tokens authenticate MCP and external clients on your behalf. They cannot exceed your own permissions.",
"ConfirmDeleteApiToken": "Revoke API token '{{name}}'? This cannot be undone.",
"ApiTokenRevoked": "API token revoked.",
"ApiTokenCreated": "API token created",
"ApiTokenRevealOnce": "Copy this token now. It will never be shown again.",
"ApiTokenStoreSafely": "Treat this token like a password. Anyone with it can act on your behalf within its scopes.",
"ApiTokenServers": "Servers",
"ApiTokenAllServers": "all permitted",
"ApiTokenNever": "never",
"ApiTokenExpiresAt": "Expires",
"ApiTokenLastUsed": "Last used",
"ApiTokenServerIDs": "Restrict to server IDs (optional)",
"ApiTokenServerIDsPlaceholder": "comma-separated, e.g. 1,2,3",
"ApiTokenExpiresInDays": "Expires in (days, 0 = never)",
"ApiTokenScopeRequired": "At least one scope is required.",
"ApiTokenServersInvalid": "Server IDs must be positive integers.",
"ApiTokenExpiryInvalid": "Days must be between 0 and 3650.",
"NameRequired": "Name is required.",
"Revoke": "Revoke",
"Copied": "Copied to clipboard",
"Scopes": "Scopes"
}
+26 -2
View File
@@ -64,7 +64,6 @@
"Confirm": "Confirmer",
"ConfirmDeletion": "Confirmer la suppression?",
"Services": "Services",
"ShowInService": "Montrer en service",
"MaximumLatency": "Délai maximum (ms)",
"MinimumLatency": "Délai minimum (millisecondes)",
"Command": "Commande",
@@ -92,5 +91,30 @@
"UserInvalid": "Lutilisateur est invalide",
"BlockByUser": "Bloqué par un administrateur",
"OnlineUser": "Utilisateur en ligne",
"UserId": "ID utilisateur"
"UserId": "ID utilisateur",
"EnableMCP": "Enable MCP endpoint (off by default; review API token scopes and server allow-list first)",
"ApiTokens": "API Tokens",
"CreateApiToken": "Create API token",
"CreateApiTokenDescription": "Tokens authenticate MCP and external clients on your behalf. They cannot exceed your own permissions.",
"ConfirmDeleteApiToken": "Revoke API token '{{name}}'? This cannot be undone.",
"ApiTokenRevoked": "API token revoked.",
"ApiTokenCreated": "API token created",
"ApiTokenRevealOnce": "Copy this token now. It will never be shown again.",
"ApiTokenStoreSafely": "Treat this token like a password. Anyone with it can act on your behalf within its scopes.",
"ApiTokenServers": "Servers",
"ApiTokenAllServers": "all permitted",
"ApiTokenNever": "never",
"ApiTokenExpiresAt": "Expires",
"ApiTokenLastUsed": "Last used",
"ApiTokenServerIDs": "Restrict to server IDs (optional)",
"ApiTokenServerIDsPlaceholder": "comma-separated, e.g. 1,2,3",
"ApiTokenExpiresInDays": "Expires in (days, 0 = never)",
"ApiTokenScopeRequired": "At least one scope is required.",
"ApiTokenServersInvalid": "Server IDs must be positive integers.",
"ApiTokenExpiryInvalid": "Days must be between 0 and 3650.",
"NameRequired": "Name is required.",
"Revoke": "Revoke",
"Copy": "Copy",
"Copied": "Copied to clipboard",
"Scopes": "Scopes"
}
+27 -1
View File
@@ -21,5 +21,31 @@
"NoRowsAreSelected": "Non hai filas seleccionadas",
"ThisOperationIsUnrecoverable": "A operación non se pode desfacer!",
"TaskTriggeredSuccessfully": "A tarefa desencadeouse correctamente"
}
},
"EnableMCP": "Enable MCP endpoint (off by default; review API token scopes and server allow-list first)",
"ApiTokens": "API Tokens",
"CreateApiToken": "Create API token",
"CreateApiTokenDescription": "Tokens authenticate MCP and external clients on your behalf. They cannot exceed your own permissions.",
"ConfirmDeleteApiToken": "Revoke API token '{{name}}'? This cannot be undone.",
"ApiTokenRevoked": "API token revoked.",
"ApiTokenCreated": "API token created",
"ApiTokenRevealOnce": "Copy this token now. It will never be shown again.",
"ApiTokenStoreSafely": "Treat this token like a password. Anyone with it can act on your behalf within its scopes.",
"ApiTokenServers": "Servers",
"ApiTokenAllServers": "all permitted",
"ApiTokenNever": "never",
"ApiTokenExpiresAt": "Expires",
"ApiTokenLastUsed": "Last used",
"ApiTokenServerIDs": "Restrict to server IDs (optional)",
"ApiTokenServerIDsPlaceholder": "comma-separated, e.g. 1,2,3",
"ApiTokenExpiresInDays": "Expires in (days, 0 = never)",
"ApiTokenScopeRequired": "At least one scope is required.",
"ApiTokenServersInvalid": "Server IDs must be positive integers.",
"ApiTokenExpiryInvalid": "Days must be between 0 and 3650.",
"NameRequired": "Name is required.",
"Revoke": "Revoke",
"Copy": "Copy",
"Copied": "Copied to clipboard",
"Done": "Done",
"Scopes": "Scopes"
}
+62 -2
View File
@@ -83,7 +83,6 @@
"Confirm": "Konfirmasi",
"ConfirmDeletion": "Konfirmasi Penghapusan?",
"Services": "Layanan",
"ShowInService": "Tampilkan di Layanan",
"Coverages": {
"Excludes": "Kecualikan Server Tertentu",
"Only": "Hanya Server Tertentu",
@@ -139,6 +138,10 @@
"WAF": "Firewall Aplikasi Web",
"SiteName": "Nama Situs",
"DashboardOriginalHost": "Alamat koneksi Agen [nama domain/IP:port]",
"DashboardHost": "Host dashboard untuk callback OAuth2 [nama domain/IP:port]",
"DashboardHostHint": "Host publik dashboard yang dipakai untuk membangun URL callback OAuth2. Atur ini bila dashboard diakses pada domain yang berbeda dari alamat koneksi Agen. Kosongkan untuk mempercayai Host permintaan apa adanya.",
"ReservedHosts": "Host khusus (dipisahkan koma; domain publik/reverse-proxy yang tidak boleh dipakai anggota sebagai domain NAT)",
"ReservedHostsHint": "Di belakang reverse proxy, dasbor tidak melihat domain publiknya sendiri sehingga tidak terlindungi otomatis. Cantumkan setiap host entri publik di sini, atau anggota bisa mendaftarkannya sebagai domain NAT dan membajak perutean dasbor.",
"ConfigTLS": "Gunakan TLS untuk menghubungkan Agen",
"LoginFailed": "Gagal Masuk",
"BruteForceAttackingToken": "Token Serangan Brute Force",
@@ -255,5 +258,62 @@
"ClipboardWriteFailed": "Gagal menulis ke papan klip",
"PastedFromClipboard": "Ditempel dari papan klip",
"ClipboardReadFailed": "Gagal membaca papan klip",
"FormatMetricUnits": "Format Satuan Metrik"
"FormatMetricUnits": "Format Satuan Metrik",
"EnableMCP": "Enable MCP endpoint (off by default; review API token scopes and server allow-list first)",
"ApiTokens": "API Tokens",
"CreateApiToken": "Create API token",
"CreateApiTokenDescription": "Tokens authenticate MCP and external clients on your behalf. They cannot exceed your own permissions.",
"ConfirmDeleteApiToken": "Revoke API token '{{name}}'? This cannot be undone.",
"ApiTokenRevoked": "API token revoked.",
"ApiTokenCreated": "API token created",
"ApiTokenRevealOnce": "Copy this token now. It will never be shown again.",
"ApiTokenStoreSafely": "Treat this token like a password. Anyone with it can act on your behalf within its scopes.",
"ApiTokenServers": "Servers",
"ApiTokenAllServers": "all permitted",
"ApiTokenNever": "never",
"ApiTokenExpiresAt": "Expires",
"ApiTokenLastUsed": "Last used",
"ApiTokenServerIDs": "Restrict to server IDs (optional)",
"ApiTokenServerIDsPlaceholder": "comma-separated, e.g. 1,2,3",
"ApiTokenExpiresInDays": "Expires in (days, 0 = never)",
"ApiTokenScopeRequired": "At least one scope is required.",
"ApiTokenServersInvalid": "Server IDs must be positive integers.",
"ApiTokenExpiryInvalid": "Days must be between 0 and 3650.",
"NameRequired": "Name is required.",
"Revoke": "Revoke",
"Copied": "Copied to clipboard",
"Scopes": "Scopes",
"Owner": "Pemilik",
"GlobalAgent": "Agen Global",
"UnknownUser": "Pengguna tidak dikenal (#{{id}})",
"Status": "Status",
"ServerID": "ID Server",
"CreatedAt": "Dibuat Pada",
"BatchMoveServer": "Pindahkan Pemilik Server Secara Batch",
"Servers": "Server",
"ToUser": "Ke Pengguna",
"Move": "Pindahkan",
"Transfer": {
"Title": "Transfer Server",
"PageHint": "Melacak transfer kepemilikan yang dimulai dari tindakan pindah batch. Entri yang masih tertunda dapat dibatalkan; entri terminal dapat dicoba lagi.",
"From": "Dari",
"To": "Ke",
"Initiator": "Pemrakarsa",
"LastError": "Kesalahan Terakhir",
"Retry": "Coba Lagi",
"CancelRequested": "Permintaan pembatalan dikirim",
"RetryRequested": "Permintaan coba lagi dikirim",
"BatchSubmitted": "Transfer batch dikirim",
"PendingCount": "{{count}} tertunda",
"PermissionDeniedCount": "{{count}} izin ditolak",
"AlreadyTransferringCount": "{{count}} sudah dalam proses transfer",
"ServerNotFoundCount": "{{count}} tidak ditemukan",
"SameOwnerCount": "{{count}} sudah dimiliki oleh target",
"AgentTooOldCount": "{{count}} agen terlalu lama versinya (perlu upgrade)",
"StatusPending": "Tertunda",
"StatusVerified": "Terverifikasi",
"StatusFailed": "Gagal",
"StatusTimeout": "Batas waktu habis",
"StatusCancelled": "Dibatalkan"
}
}
+30 -2
View File
@@ -73,7 +73,6 @@
"Confirm": "Confermo",
"ConfirmDeletion": "Confermi l'eliminazione?",
"Services": "Servizi",
"ShowInService": "Mostra in servizio",
"Coverages": {
"Only": "Solo server specifici",
"Excludes": "Escludi server specifici",
@@ -142,6 +141,10 @@
"CustomCodes": "Codice personalizzato (stili e script)",
"CustomCodesDashboard": "Codice personalizzato per dashboard",
"DashboardOriginalHost": "Indirizzo di ancoraggio dell'agente [nome dominio/IP:porta]",
"DashboardHost": "Host della dashboard per il callback OAuth2 [nome dominio/IP:porta]",
"DashboardHostHint": "Host pubblico della dashboard usato per costruire l'URL di callback OAuth2. Impostalo quando la dashboard è raggiungibile su un dominio diverso dall'indirizzo di connessione dell'agente. Lascia vuoto per fidarti dell'Host della richiesta così com'è.",
"ReservedHosts": "Host riservati (separati da virgola; domini pubblici/reverse-proxy che i membri non possono usare come dominio NAT)",
"ReservedHostsHint": "Dietro un reverse proxy il pannello non vede il proprio dominio pubblico, quindi non è protetto automaticamente. Elenca qui ogni hostname di ingresso pubblico, altrimenti un membro potrebbe registrarlo come dominio NAT e dirottare il routing del pannello.",
"ConfigTLS": "Usa TLS per connettere Agent",
"CustomPublicDNSNameserversforDDNS": "Server dei nomi DNS pubblici personalizzati per DDNS",
"WebRealIPHeader": "Intestazione della richiesta IP reale del frontend",
@@ -175,5 +178,30 @@
"OnlineUser": "Utente online",
"BlockIdentifier": "Identificatore del blocco",
"UserId": "ID utente",
"ConnectedAt": "Connesso alle"
"ConnectedAt": "Connesso alle",
"EnableMCP": "Enable MCP endpoint (off by default; review API token scopes and server allow-list first)",
"ApiTokens": "API Tokens",
"CreateApiToken": "Create API token",
"CreateApiTokenDescription": "Tokens authenticate MCP and external clients on your behalf. They cannot exceed your own permissions.",
"ConfirmDeleteApiToken": "Revoke API token '{{name}}'? This cannot be undone.",
"ApiTokenRevoked": "API token revoked.",
"ApiTokenCreated": "API token created",
"ApiTokenRevealOnce": "Copy this token now. It will never be shown again.",
"ApiTokenStoreSafely": "Treat this token like a password. Anyone with it can act on your behalf within its scopes.",
"ApiTokenServers": "Servers",
"ApiTokenAllServers": "all permitted",
"ApiTokenNever": "never",
"ApiTokenExpiresAt": "Expires",
"ApiTokenLastUsed": "Last used",
"ApiTokenServerIDs": "Restrict to server IDs (optional)",
"ApiTokenServerIDsPlaceholder": "comma-separated, e.g. 1,2,3",
"ApiTokenExpiresInDays": "Expires in (days, 0 = never)",
"ApiTokenScopeRequired": "At least one scope is required.",
"ApiTokenServersInvalid": "Server IDs must be positive integers.",
"ApiTokenExpiryInvalid": "Days must be between 0 and 3650.",
"NameRequired": "Name is required.",
"Revoke": "Revoke",
"Copy": "Copy",
"Copied": "Copied to clipboard",
"Scopes": "Scopes"
}
+28 -1
View File
@@ -1 +1,28 @@
{}
{
"EnableMCP": "Enable MCP endpoint (off by default; review API token scopes and server allow-list first)",
"ApiTokens": "API Tokens",
"CreateApiToken": "Create API token",
"CreateApiTokenDescription": "Tokens authenticate MCP and external clients on your behalf. They cannot exceed your own permissions.",
"ConfirmDeleteApiToken": "Revoke API token '{{name}}'? This cannot be undone.",
"ApiTokenRevoked": "API token revoked.",
"ApiTokenCreated": "API token created",
"ApiTokenRevealOnce": "Copy this token now. It will never be shown again.",
"ApiTokenStoreSafely": "Treat this token like a password. Anyone with it can act on your behalf within its scopes.",
"ApiTokenServers": "Servers",
"ApiTokenAllServers": "all permitted",
"ApiTokenNever": "never",
"ApiTokenExpiresAt": "Expires",
"ApiTokenLastUsed": "Last used",
"ApiTokenServerIDs": "Restrict to server IDs (optional)",
"ApiTokenServerIDsPlaceholder": "comma-separated, e.g. 1,2,3",
"ApiTokenExpiresInDays": "Expires in (days, 0 = never)",
"ApiTokenScopeRequired": "At least one scope is required.",
"ApiTokenServersInvalid": "Server IDs must be positive integers.",
"ApiTokenExpiryInvalid": "Days must be between 0 and 3650.",
"NameRequired": "Name is required.",
"Revoke": "Revoke",
"Copy": "Copy",
"Copied": "Copied to clipboard",
"Done": "Done",
"Scopes": "Scopes"
}
+3
View File
@@ -0,0 +1,3 @@
{
"nezha": "Nezha Monitoramento"
}
+314
View File
@@ -0,0 +1,314 @@
{
"nezha": "Monitor-ul Nezha",
"theme": {
"light": "Mod luminos",
"dark": "Mod întunecat",
"system": "Monitorizeaza sistem-ul"
},
"Username": "Nume de utilizator",
"Password": "Parolă",
"InvalidUsernameOrPassword": "Nume de utilizator sau parolă incorectă",
"NetworkError": "Problemă cu rețeaua",
"LoginFirst": "Autentificati-vă, pentru a continua",
"CurrentTime": "Ora actuală",
"Results": {
"UsernameMin": "Numele de utilizator trebuie să conține cel puțin {{number}} de caractere lung.",
"PasswordRequired": "Parola nu poate fii goală.",
"ErrorFetchingResource": "Error Fetching Resource: {{error}}",
"SelectAtLeastOneServer": "Alege-ți cel puțin un server.",
"UnExpectedError": "Eroare neașteptată, verificați consola pentru mai multe informații.",
"ForceUpdate": "Upgradare forțată:",
"NoRowsAreSelected": "Nu este niciun rând selectat",
"ThisOperationIsUnrecoverable": "Această acțiune este ireversibilă!",
"TaskTriggeredSuccessfully": "Sarcina a fost declanșată cu succes",
"TheServerDoesNotOnline": "Server-ul nu exista sau nu a fost incă connectat",
"InstallHostRequired": "Adresa de connectare a agentului nu a fost incă completată in setări.",
"UnknownIdentifier": "Identificator necunoscut"
},
"Login": "Autentificare",
"Server": "Server",
"Service": "Service",
"Task": "Sarcină",
"Notification": "Notificare",
"DDNS": "DNS dinamic",
"NATT": "Traversare NAT",
"Group": "Grupă",
"Profile": "Profil",
"Settings": "Setări sistem",
"BackToHome": "Spre pagina principală",
"Logout": "Deconectare",
"NavigateTo": "Spre",
"SelectAPageToNavigateTo": "Du-te la pagina",
"Close": "Închide",
"Error": "Eroare",
"Name": "Nume",
"Version": "Versiune",
"Unknown": "Necunoscut",
"Enable": "Permite",
"HideForGuest": "Ascuns de vizitatori",
"InstallCommands": "Comanda de instalare",
"Terminal": "Terminal",
"Config": "Config",
"Note": "Notiță",
"Success": "Succes",
"Done": "Finalizat",
"Offline": "Offline",
"Failure": "Eșuare",
"Loading": "Se incarcă",
"NoResults": "Niciun resultat",
"Actions": "Acțiuni",
"EditServer": "Editează server-ul",
"Weight": "Greutate (cu căt mai mare este numărul, cu atâta de sus este afișat)",
"DDNSProfiles": "ID-urile de profil DDNS",
"SeparateWithComma": "(Separă cu virgulă)",
"Public": "Public",
"Private": "Privat",
"Submit": "Trimite",
"Target": "Obiectiv",
"Coverage": "Acoperire",
"CoverAll": "Acoperă-le pe toate",
"IgnoreAll": "Ignoră tot",
"OnAlert": "Server-ele alarmate",
"SpecificServers": "Server specific",
"Type": "Tip",
"Interval": "Interval",
"NotifierGroupID": "Notifier Group ID",
"Trigger": "La declanșare",
"TasksToTriggerOnAlert": "Sarcini, care sa fie declanșate la alerta",
"TasksToTriggerAfterRecovery": "Sarcini, care sa fie declanșate după recuperare",
"Add": "Adaugă",
"Delete": "Sterge",
"AdvancedJSON": "JSON avansat",
"Save": "Salvează",
"Confirm": "Confirmă",
"ConfirmDeletion": "Confirmă stergerea?",
"Services": "Servicii",
"ShowInService": "Deschide in servicii",
"Coverages": {
"Excludes": "Exclude servere specifice",
"Only": "Numai servere specifice",
"Alarmed": "Executate pe server-ul care declanșeaza alarma"
},
"EnableFailureNotification": "Activează notificare la eșec",
"MaximumLatency": "Delay-ul maximal (ms)",
"MinimumLatency": "Delay-ul minimal (ms)",
"EnableLatencyNotification": "Activează notificare de latență",
"EnableTriggerTask": "Activează sarcină de declanșare",
"CronExpression": "Expresii Cron",
"Command": "Commandă",
"NotifierGroup": "Grupa, care sa fie informată",
"SendSuccessNotification": "Trimite notificare de succes",
"LastExecution": "Ultima execuție",
"Result": "Rezultatul",
"Scheduled": "Sarcină programată",
"AlertRule": "Reguli de alertă",
"VerifyTLS": "Verifică TLS-ul",
"TriggerMode": "Modul de declanșare",
"Rules": "Reguli",
"RequestMethod": "Metoda de request",
"RequestHeader": "Header-ul de request",
"DoNotSendTestMessage": "Nu trimite mesaj de test",
"Always": "Mereu",
"Once": "Odată",
"Provider": "Furnizor",
"Domains": "Domenii",
"MaximumRetryAttempts": "Durata maximă pentru încercările de reluare",
"Refresh": "Reincarcă",
"CopyPath": "Copiază calea",
"Goto": "Accesează",
"UpdateProfile": "Actualizează profil-ul",
"NewUsername": "Nume de utilizator nou",
"OriginalPassword": "Parola originală",
"NewPassword": "Parola noua",
"EditDDNS": "Modifică DDNS-ul",
"CreateDDNS": "Crează un DDNS",
"RequestType": "Tip-ul de request",
"RequestBody": "Request Body",
"FileManager": "Pseudo manager de fișiere",
"Downloading": "Se descarcă",
"Uploading": "Se încarcă",
"EditNAT": "Modifică configurarea NAT",
"CreateNAT": "Crează o configurare NAT",
"LocalService": "Serviciu local",
"BindHostname": "Leagă numele de domeniu",
"EditServerGroup": "Modifică grupa de servere",
"CreateServerGroup": "Crează o grupă de servere",
"User": "Utilizator",
"Owner": "Propietar",
"GlobalAgent": "Agent global",
"UnknownUser": "Utilizator necunoscut (#{{id}})",
"WAF": "Web Application Firewall",
"SiteName": "Numele site-ului",
"DashboardOriginalHost": "Adresa de conectare a agentului [numele domeniului/IP:port]",
"DashboardHost": "Host-ul panoului pentru callback-ul OAuth2 [numele domeniului/IP:port]",
"DashboardHostHint": "Host-ul public al panoului folosit pentru a construi URL-ul de callback OAuth2. Setați-l când panoul este accesat pe un domeniu diferit de adresa de conectare a agentului. Lăsați gol pentru a avea încredere în Host-ul cererii ca atare.",
"ReservedHosts": "Host-uri reservate (separate prin virgulă; domeniile publice sau ale reverse proxy-urilor nu pot fi utilizate ca domenii NAT)",
"ReservedHostsHint": "În spatele unui reverse proxy, dashboard-ul nu își poate vedea propriul domeniu public, prin urmare nu este protejat automat. Trebuie să menționați aici toate hostname-urile ale intrărilor publice; în caz contrar, un membru ar putea să le înregistreze ca domenii NAT și să deturneze rutarea către dashboard.",
"ConfigTLS": "Folosește TLS pentru a conecta agentul",
"LoginFailed": "Autentificarea a eșuat",
"BruteForceAttackingToken": "Brute force atacează token-ul",
"BruteForceAttackingAgentSecret": "Brute force atacează agentul secret",
"Language": "Limbă",
"CustomCodes": "Cod custom (Style și script)",
"CustomCodesDashboard": "Cod custom pentru dashboard",
"CustomPublicDNSNameserversforDDNS": "Servere de nume DNS publice personalizate pentru DDNS",
"WebRealIPHeader": "adresa reală IP frontend request header",
"AgentRealIPHeader": "adresa reală IP a agentului request header",
"UseDirectConnectingIP": "Folosește conexiune IP directă",
"IPChangeNotification": "Notificare la schimbare de adresa IP",
"FullIPNotification": "Afișează intreaga adresa IP in notificări",
"EnableMCP": "Activează endpoint-uri MCP (implicit dezactivate; Verificați mai întâi domeniile de aplicare ale tokenurilor API și lista de servere permise)",
"EditService": "Modifică serviciul",
"CreateService": "Creează un serviciu",
"EditTask": "Modifică sarcina",
"CreateTask": "Creează sarcină",
"CreateNotifier": "Creează un notificator",
"EditNotifier": "Modifică notificatorul",
"EditAlertRule": "Modifică regurile de alertare",
"CreateAlertRule": "Creează reguli de alertare",
"EditNotifierGroup": "Modifică grupa, care va fii notificată",
"CreateNotifierGroup": "Creează o grupă de notificare",
"NewUser": "Utilizator nou",
"Count": "Numără",
"LastBlockReason": "Motivul ultimului block",
"LastBlockTime": "Ultima perioadă de ban",
"Theme": "Temă",
"Author": "Autor",
"Repository": "Repository",
"Community": "Comunitate",
"Official": "Oficial",
"CommunityThemeWarning": "Utilizezi momentan o temă creată de către comunitate",
"CommunityThemeDescription": "Această temă este dezvoltată de către comunitate, o folosiți pe propria răspundere",
"Cancel": "Anulare",
"EnableDDNS": "Activează DDNS",
"PushSuccessful": "Push dacă a fost cu sucess",
"GrpcAuthFailed": "Autentificarea gRPC a eșuat",
"APITokenInvalid": "Token-ul API este invalid",
"UserInvalid": "Utilizator-ul este invalid",
"BlockByUser": "Blocat de către administrator",
"BlockIdentifier": "Identificator de block",
"UserId": "User ID",
"ConnectedAt": "Ultima dată conectat pe",
"OnlineUser": "Utilizator activ",
"Total": "Total",
"ConfirmBlock": "Confirmă blocarea",
"RejectPassword": "Respinge logarea cu parolă",
"EmptyText": "Text este gol",
"EmptyNote": "Nu ai nici-o notiță.",
"OverrideDDNSDomains": "Suprascriere domenii DDNS (per configurație)",
"EditServerConfig": "Modifică configurarea server-ului",
"Option": "Optiune",
"Value": "Valoare",
"Preview": "Previzualizare",
"PublicNote": {
"Label": "Notiță publică",
"Billing": "Facturare",
"Plan": "Tarif",
"StartDate": "Data de incepere",
"EndDate": "Date de finalizare",
"AutoRenewal": "Reînnoire automată",
"Cycle": "Ciclu",
"Amount": "Sumă",
"Bandwidth": "Bandwidth",
"TrafficVolume": "Volumul traficului",
"TrafficType": "Tipul traficului",
"IPv4": "IPv4",
"IPv6": "IPv6",
"NetworkRoute": "Rută network",
"Extra": "Extra",
"Enabled": "Activat",
"Disabled": "Dezactivat",
"Inbound": "Detaliat",
"Both": "Amândouă",
"Day": "Zi",
"Week": "Săptămână",
"Month": "Lună",
"Year": "An",
"NoExpiry": "Nici-o expirare",
"SetNoExpiry": "Setează nici-o expirare",
"CancelNoExpiry": "Anulează nici-o expirare",
"Free": "Liber",
"PayAsYouGo": "Plătești pe măsură ce consumi (Pay as you go)",
"CommaSeparated": "Separă mai multe obiecte cu virgulă",
"Has": "Are",
"None": "Nimica",
"CustomFields": "Câmpuri personalizate",
"ClearDate": "Sterge data",
"Clear": "Curăță",
"RawText": "Raw Text"
},
"Validation": {
"InvalidDate": "Dată invalidă",
"MustBe0Or1": "Trebuie să fie 0 sau 1",
"MustBeDayWeekMonthYear": "Trebuie să fie Zi/Săptămână/Lună/An",
"MustBe1Or2": "Trebuie să fie 1 sau 2",
"DigitsOnly": "Doar cifre",
"InvalidForm": "Formular invalid",
"InvalidJSON": "JSON invalid"
},
"AlertRules": {
"CoverAllServers": "Monitorizează toate server-ele",
"IgnoreAllSelectSpecific": "Ignoră totul, selectează server specifice",
"IgnoreHint": "{{server}} ID: adevărat/fals",
"IgnoreExample": "de exemplu, {\"1\": true, \"2\": false}"
},
"Search": "Caută...",
"Format": "Formatează",
"Formatted": "Formatat",
"Copy": "Copiază",
"Paste": "Lipește",
"CopiedToClipboard": "Copiat in clipboard",
"ClipboardWriteFailed": "Eșec la scrierea în clipboard",
"PastedFromClipboard": "Lipit din clipboard",
"ClipboardReadFailed": "Eșec la citirea clipboard-ului",
"FormatMetricUnits": "Format unități metrice",
"Status": "Status",
"ServerID": "ID-ul server-ului",
"CreatedAt": "Creat la",
"BatchMoveServer": "Mutarea în bloc a proprietarului serverului",
"Servers": "Servere",
"ToUser": "Spre utilizator",
"Move": "Transferă",
"Transfer": {
"Title": "Transfere de server",
"PageHint": "Urmărește transferurile de proprietate inițiate prin acțiunea de mutare în bloc. Înregistrările în așteptare pot fi anulate, iar cele finalizate pot fi reluate.",
"From": "De la",
"To": "Pentru",
"Initiator": "Inițiator",
"LastError": "Ultima eroare",
"Retry": "Reîncearcă",
"CancelRequested": "Anularea a fost solicitată",
"RetryRequested": "Reîncercarea a fost solicitată",
"BatchSubmitted": "Transferul în bloc a fost trimis",
"PendingCount": "{{count}} în așteptare",
"PermissionDeniedCount": "{{count}} a fost refuzat accesul",
"AlreadyTransferringCount": "{{count}} se transferă deja",
"ServerNotFoundCount": "{{count}} nu au fost găsite",
"SameOwnerCount": "{{count}} sunt deja deținute de destinatar",
"AgentTooOldCount": "{{count}} agenți sunt prea vechi (este necesară o actualizare)",
"StatusPending": "În așteptare",
"StatusVerified": "Verificat",
"StatusFailed": "A eșuat",
"StatusTimeout": "Timeout",
"StatusCancelled": "Anulat"
},
"ApiTokens": "Token-uri API",
"CreateApiToken": "Creează un token API",
"CreateApiTokenDescription": "Tokenurile autentifică MCP și clienții externi în numele dumneavoastră. Acestea nu pot depăși limitele permisiunilor dumneavoastră.",
"ConfirmDeleteApiToken": "Revocă API token-ul '{{name}}'? Această acțiune eset ireversibilă.",
"ApiTokenRevoked": "API token-ul a fost revocat.",
"ApiTokenCreated": "API token-ul a fost creat",
"ApiTokenRevealOnce": "Copiază acest token acum. Nu va mai fi afișat niciodată.",
"ApiTokenStoreSafely": "Tratează acest token ca pe o parolă. Oricine îl deține poate acționa în numele tău în limitele de aplicare ale acestuia.",
"ApiTokenServers": "Servere",
"ApiTokenAllServers": "Toate permise",
"ApiTokenNever": "Niciodată",
"ApiTokenExpiresAt": "Expiră",
"ApiTokenLastUsed": "Ultima dată utilizat",
"ApiTokenServerIDs": "Limitare la ID-urile serverelor (opțional)",
"ApiTokenServerIDsPlaceholder": "separate prin virgulă, de exemplu 1,2,3",
"ApiTokenExpiresInDays": "Expiră in (zile, 0=niciodată)",
"Revoke": "Revocă",
"Copied": "Copiat în clipboard",
"Scopes": "Scope-uri"
}
+30 -2
View File
@@ -42,6 +42,10 @@
"Credential": "Учетные данные",
"CommunityThemeDescription": "Эта тема предоставлена сообществом, используйте на свой страх и риск",
"DashboardOriginalHost": "Адрес подключения агента [домен/IP:порт]",
"DashboardHost": "Хост панели для OAuth2 callback [домен/IP:порт]",
"DashboardHostHint": "Публичный хост панели, используемый для формирования URL обратного вызова OAuth2. Укажите его, если панель доступна на домене, отличном от адреса подключения агента. Оставьте пустым, чтобы доверять Host из запроса как есть.",
"ReservedHosts": "Зарезервированные хосты (через запятую; публичные/реверс-прокси домены, которые участники не могут использовать как NAT-домен)",
"ReservedHostsHint": "За реверс-прокси панель не видит собственный публичный домен и не защищается автоматически. Укажите здесь все публичные входные хосты, иначе участник сможет зарегистрировать такой домен как NAT-домен и перехватить маршрутизацию панели.",
"Error": "Ошибка",
"TriggerMode": "Режим срабатывания",
"Once": "Один раз",
@@ -176,7 +180,6 @@
"Language": "Язык",
"ConfirmDeletion": "Подтвердить удаление?",
"OriginalPassword": "Старый пароль",
"ShowInService": "Показывать в Сервисе",
"EnableFailureNotification": "Включить уведомления об ошибках",
"CreateNAT": "Создать NAT-конфигурацию",
"Scheduled": "Запланированные задачи",
@@ -185,5 +188,30 @@
"IPChangeNotification": "Уведомление об изменении IP",
"CreateAlertRule": "Создать правила оповещений",
"UserId": "ID пользователя",
"NewUser": "Новый пользователь"
"NewUser": "Новый пользователь",
"EnableMCP": "Enable MCP endpoint (off by default; review API token scopes and server allow-list first)",
"ApiTokens": "API Tokens",
"CreateApiToken": "Create API token",
"CreateApiTokenDescription": "Tokens authenticate MCP and external clients on your behalf. They cannot exceed your own permissions.",
"ConfirmDeleteApiToken": "Revoke API token '{{name}}'? This cannot be undone.",
"ApiTokenRevoked": "API token revoked.",
"ApiTokenCreated": "API token created",
"ApiTokenRevealOnce": "Copy this token now. It will never be shown again.",
"ApiTokenStoreSafely": "Treat this token like a password. Anyone with it can act on your behalf within its scopes.",
"ApiTokenServers": "Servers",
"ApiTokenAllServers": "all permitted",
"ApiTokenNever": "never",
"ApiTokenExpiresAt": "Expires",
"ApiTokenLastUsed": "Last used",
"ApiTokenServerIDs": "Restrict to server IDs (optional)",
"ApiTokenServerIDsPlaceholder": "comma-separated, e.g. 1,2,3",
"ApiTokenExpiresInDays": "Expires in (days, 0 = never)",
"ApiTokenScopeRequired": "At least one scope is required.",
"ApiTokenServersInvalid": "Server IDs must be positive integers.",
"ApiTokenExpiryInvalid": "Days must be between 0 and 3650.",
"NameRequired": "Name is required.",
"Revoke": "Revoke",
"Copy": "Copy",
"Copied": "Copied to clipboard",
"Scopes": "Scopes"
}
+30 -2
View File
@@ -25,7 +25,6 @@
},
"ConfirmDeletion": "நீக்குதலை உறுதிப்படுத்தவா?",
"Services": "சேவைகள்",
"ShowInService": "சேவையில் காட்டு",
"Coverages": {
"Excludes": "குறிப்பிட்ட சேவையகங்களை விலக்குகிறது",
"Only": "குறிப்பிட்ட சேவையகங்கள் மட்டுமே",
@@ -135,6 +134,10 @@
"WAF": "வலை பயன்பாடு ஃபயர்வால்",
"SiteName": "தளத்தின் பெயர்",
"DashboardOriginalHost": "முகவரியை இணைக்கும் முகவரி [டொமைன் பெயர்/ஐபி: போர்ட்]",
"DashboardHost": "OAuth2 கால்பேக்கிற்கான டாஷ்போர்டு ஹோஸ்ட் [டொமைன் பெயர்/ஐபி: போர்ட்]",
"DashboardHostHint": "OAuth2 கால்பேக் URL-ஐ உருவாக்கப் பயன்படும் டாஷ்போர்டின் பொது ஹோஸ்ட். ஏஜெண்ட் இணைப்பு முகவரியிலிருந்து வேறுபட்ட டொமைனில் டாஷ்போர்டு அணுகப்பட்டால் இதை அமைக்கவும். கோரிக்கையின் Host-ஐ அப்படியே நம்ப காலியாக விடவும்.",
"ReservedHosts": "ஒதுக்கப்பட்ட ஹோஸ்ட்கள் (கமாவால் பிரிக்கப்பட்டது; உறுப்பினர்கள் NAT டொமைனாகப் பயன்படுத்த முடியாத பொது/ரிவர்ஸ்-ப்ராக்ஸி டொமைன்கள்)",
"ReservedHostsHint": "ரிவர்ஸ்-ப்ராக்ஸிக்குப் பின்னால் டாஷ்போர்டு அதன் சொந்த பொது டொமைனைப் பார்க்க முடியாது, எனவே அது தானாகப் பாதுகாக்கப்படாது. அனைத்து பொது நுழைவு ஹோஸ்ட்களையும் இங்கே பட்டியலிடுங்கள்; இல்லையெனில் ஒரு உறுப்பினர் அதை NAT டொமைனாகப் பதிவுசெய்து டாஷ்போர்டு வழித்தடத்தைக் கடத்தலாம்.",
"ConfigTLS": "முகவரை இணைக்க TLS ஐப் பயன்படுத்தவும்",
"LoginFailed": "உள்நுழைவு தோல்வியடைந்தது",
"BruteForceAttackingToken": "மிருகத்தனமான ஆற்றல் டோக்கனைத் தாக்குகிறது",
@@ -180,5 +183,30 @@
"ConfirmBlock": "தொகுதி உறுதிப்படுத்தவும்",
"RejectPassword": "கடவுச்சொல் உள்நுழைவை நிராகரிக்கவும்",
"EmptyText": "உரை காலியாக உள்ளது",
"EmptyNote": "உங்களிடம் எந்த குறிப்பும் இல்லை."
"EmptyNote": "உங்களிடம் எந்த குறிப்பும் இல்லை.",
"EnableMCP": "Enable MCP endpoint (off by default; review API token scopes and server allow-list first)",
"ApiTokens": "API Tokens",
"CreateApiToken": "Create API token",
"CreateApiTokenDescription": "Tokens authenticate MCP and external clients on your behalf. They cannot exceed your own permissions.",
"ConfirmDeleteApiToken": "Revoke API token '{{name}}'? This cannot be undone.",
"ApiTokenRevoked": "API token revoked.",
"ApiTokenCreated": "API token created",
"ApiTokenRevealOnce": "Copy this token now. It will never be shown again.",
"ApiTokenStoreSafely": "Treat this token like a password. Anyone with it can act on your behalf within its scopes.",
"ApiTokenServers": "Servers",
"ApiTokenAllServers": "all permitted",
"ApiTokenNever": "never",
"ApiTokenExpiresAt": "Expires",
"ApiTokenLastUsed": "Last used",
"ApiTokenServerIDs": "Restrict to server IDs (optional)",
"ApiTokenServerIDsPlaceholder": "comma-separated, e.g. 1,2,3",
"ApiTokenExpiresInDays": "Expires in (days, 0 = never)",
"ApiTokenScopeRequired": "At least one scope is required.",
"ApiTokenServersInvalid": "Server IDs must be positive integers.",
"ApiTokenExpiryInvalid": "Days must be between 0 and 3650.",
"NameRequired": "Name is required.",
"Revoke": "Revoke",
"Copy": "Copy",
"Copied": "Copied to clipboard",
"Scopes": "Scopes"
}
+26 -2
View File
@@ -83,7 +83,6 @@
"Confirm": "Підтвердити",
"ConfirmDeletion": "Підтвердити видалення?",
"Services": "Сервіси",
"ShowInService": "Показувати у Сервісі",
"Coverages": {
"Excludes": "Виключити певні сервери",
"Only": "Тільки певні сервери",
@@ -127,5 +126,30 @@
"RequestType": "Тип запиту",
"RequestBody": "Тіло запиту",
"FileManager": "Псевдо Менеджер файлів",
"Downloading": "Завантаження"
"Downloading": "Завантаження",
"EnableMCP": "Enable MCP endpoint (off by default; review API token scopes and server allow-list first)",
"ApiTokens": "API Tokens",
"CreateApiToken": "Create API token",
"CreateApiTokenDescription": "Tokens authenticate MCP and external clients on your behalf. They cannot exceed your own permissions.",
"ConfirmDeleteApiToken": "Revoke API token '{{name}}'? This cannot be undone.",
"ApiTokenRevoked": "API token revoked.",
"ApiTokenCreated": "API token created",
"ApiTokenRevealOnce": "Copy this token now. It will never be shown again.",
"ApiTokenStoreSafely": "Treat this token like a password. Anyone with it can act on your behalf within its scopes.",
"ApiTokenServers": "Servers",
"ApiTokenAllServers": "all permitted",
"ApiTokenNever": "never",
"ApiTokenExpiresAt": "Expires",
"ApiTokenLastUsed": "Last used",
"ApiTokenServerIDs": "Restrict to server IDs (optional)",
"ApiTokenServerIDsPlaceholder": "comma-separated, e.g. 1,2,3",
"ApiTokenExpiresInDays": "Expires in (days, 0 = never)",
"ApiTokenScopeRequired": "At least one scope is required.",
"ApiTokenServersInvalid": "Server IDs must be positive integers.",
"ApiTokenExpiryInvalid": "Days must be between 0 and 3650.",
"NameRequired": "Name is required.",
"Revoke": "Revoke",
"Copy": "Copy",
"Copied": "Copied to clipboard",
"Scopes": "Scopes"
}
+62 -2
View File
@@ -83,7 +83,6 @@
"Confirm": "确认",
"ConfirmDeletion": "确认删除?",
"Services": "服务",
"ShowInService": "服务中显示",
"Coverages": {
"Excludes": "排除特定服务器",
"Only": "仅特定服务器",
@@ -146,12 +145,19 @@
"EditNotifierGroup": "编辑通知分组",
"CreateNotifierGroup": "创建通知分组",
"User": "用户",
"Owner": "所属用户",
"GlobalAgent": "全局 Agent",
"UnknownUser": "未知用户 (#{{id}})",
"WAF": "Web应用防火墙",
"SiteName": "站点名称",
"Language": "语言",
"CustomCodes": "自定义代码(样式和脚本)",
"CustomCodesDashboard": "仪表板的自定义代码",
"DashboardOriginalHost": "Agent对接地址【域名/IP:端口】",
"DashboardHost": "面板 OAuth2 回调域名【域名/IP:端口】",
"DashboardHostHint": "用于生成 OAuth2 回调地址的面板对外域名。当面板访问域名与 Agent 对接地址不同时设置此项。留空则信任请求 Host,不做强制重写。",
"ReservedHosts": "保留 Host(逗号分隔;反代/公网域名,成员不可注册为 NAT 域名)",
"ReservedHostsHint": "反代部署下面板看不到自身的对外域名,无法自动保护。请在此列出所有公网入口域名,否则成员可将其注册为 NAT 域名抢占面板路由。",
"ConfigTLS": "Agent 使用 TLS 连接",
"CustomPublicDNSNameserversforDDNS": "DDNS 的自定义公共 DNS 名称服务器",
"WebRealIPHeader": "前端真实IP请求头",
@@ -164,6 +170,7 @@
"DomainNotificationDays": "域名到期提醒天数",
"ServerNotificationDays": "VPS到期提醒天数",
"FullIPNotification": "在通知消息中显示完整的 IP 地址",
"EnableMCP": "启用 MCP 接入(默认关闭;启用前请确认已审阅 API 令牌 scope 与服务器白名单)",
"LoginFailed": "登录失败",
"BruteForceAttackingToken": "暴力攻击令牌",
"BruteForceAttackingAgentSecret": "暴力攻击代理秘密",
@@ -260,5 +267,58 @@
"ClipboardWriteFailed": "无法写入剪贴板",
"PastedFromClipboard": "已从剪贴板粘贴",
"ClipboardReadFailed": "无法读取剪贴板",
"FormatMetricUnits": "格式化数据单位"
"FormatMetricUnits": "格式化数据单位",
"Status": "状态",
"ServerID": "服务器 ID",
"CreatedAt": "创建时间",
"BatchMoveServer": "批量转移服务器所有者",
"Servers": "服务器",
"ToUser": "目标用户",
"Move": "转移",
"Transfer": {
"Title": "服务器转移",
"PageHint": "跟踪由批量转移操作发起的所有权转移。待处理条目可以取消;已终止条目可以重试。",
"From": "源用户",
"To": "目标用户",
"Initiator": "发起者",
"LastError": "最近错误",
"Retry": "重试",
"CancelRequested": "已请求取消",
"RetryRequested": "已请求重试",
"BatchSubmitted": "批量转移已提交",
"PendingCount": "{{count}} 个待处理",
"PermissionDeniedCount": "{{count}} 个权限不足",
"AlreadyTransferringCount": "{{count}} 个正在转移中",
"ServerNotFoundCount": "{{count}} 个未找到",
"SameOwnerCount": "{{count}} 个已属于目标用户",
"AgentTooOldCount": "{{count}} 个 agent 版本过低(需升级)",
"StatusPending": "待处理",
"StatusVerified": "已验证",
"StatusFailed": "已失败",
"StatusTimeout": "已超时",
"StatusCancelled": "已取消"
},
"ApiTokens": "API 令牌",
"CreateApiToken": "创建 API 令牌",
"CreateApiTokenDescription": "令牌用于 MCP 和外部客户端以你的身份调用 API,权限不能超过你本人。",
"ConfirmDeleteApiToken": "确定吊销 API 令牌 '{{name}}'?此操作不可撤销。",
"ApiTokenRevoked": "API 令牌已吊销。",
"ApiTokenCreated": "API 令牌已创建",
"ApiTokenRevealOnce": "请立即复制此令牌,离开后将无法再次查看。",
"ApiTokenStoreSafely": "请像密码一样妥善保管。任何持有者都可在 scope 内以你的身份操作。",
"ApiTokenServers": "服务器",
"ApiTokenAllServers": "全部可访问",
"ApiTokenNever": "永不",
"ApiTokenExpiresAt": "过期时间",
"ApiTokenLastUsed": "最近使用",
"ApiTokenServerIDs": "限定服务器 ID(可选)",
"ApiTokenServerIDsPlaceholder": "逗号分隔,例如 1,2,3",
"ApiTokenExpiresInDays": "有效期(天,0 = 永不过期)",
"ApiTokenScopeRequired": "至少选择一个 scope。",
"ApiTokenServersInvalid": "服务器 ID 必须为正整数。",
"ApiTokenExpiryInvalid": "天数必须在 0 到 3650 之间。",
"NameRequired": "名称必填。",
"Revoke": "吊销",
"Copied": "已复制到剪贴板",
"Scopes": "权限"
}
+65 -2
View File
@@ -75,7 +75,6 @@
"Confirm": "確認",
"ConfirmDeletion": "確認刪除?",
"Services": "服務",
"ShowInService": "服務中顯示",
"Coverages": {
"Only": "僅特定伺服器",
"Excludes": "排除特定伺服器",
@@ -138,12 +137,19 @@
"EditNotifierGroup": "編輯通知分組",
"CreateNotifierGroup": "建立通知分組",
"User": "使用者",
"Owner": "所屬使用者",
"GlobalAgent": "全域 Agent",
"UnknownUser": "未知使用者 (#{{id}})",
"WAF": "Web應用防火牆",
"SiteName": "網站名稱",
"Language": "語言",
"CustomCodes": "自訂程式碼(樣式和腳本)",
"CustomCodesDashboard": "儀表板的自訂程式碼",
"DashboardOriginalHost": "Agent對接位址【網域名稱/IP:連接埠】",
"DashboardHost": "面板 OAuth2 回呼網域【網域名稱/IP:連接埠】",
"DashboardHostHint": "用於產生 OAuth2 回呼位址的面板對外網域。當面板存取網域與 Agent 對接位址不同時設定此項。留空則信任請求 Host,不做強制重寫。",
"ReservedHosts": "保留 Host(逗號分隔;反代/公網網域,成員不可註冊為 NAT 網域)",
"ReservedHostsHint": "反代部署下面板看不到自身的對外網域,無法自動保護。請在此列出所有公網入口網域,否則成員可將其註冊為 NAT 網域搶佔面板路由。",
"ConfigTLS": "Agent 使用 TLS 連線",
"CustomPublicDNSNameserversforDDNS": "DDNS 的自訂公共 DNS 名稱伺服器",
"WebRealIPHeader": "前端真實IP請求頭",
@@ -186,5 +192,62 @@
"Option": "選項",
"Value": "值",
"Preview": "預覽",
"FormatMetricUnits": "格式化資料單位"
"FormatMetricUnits": "格式化資料單位",
"Status": "狀態",
"ServerID": "伺服器 ID",
"CreatedAt": "建立時間",
"BatchMoveServer": "批次轉移伺服器擁有者",
"Servers": "伺服器",
"ToUser": "目標使用者",
"Move": "轉移",
"Transfer": {
"Title": "伺服器轉移",
"PageHint": "追蹤由批次轉移操作發起的擁有權轉移。待處理項目可以取消;已終止項目可以重試。",
"From": "來源使用者",
"To": "目標使用者",
"Initiator": "發起者",
"LastError": "最近錯誤",
"Retry": "重試",
"CancelRequested": "已請求取消",
"RetryRequested": "已請求重試",
"BatchSubmitted": "批次轉移已提交",
"PendingCount": "{{count}} 個待處理",
"PermissionDeniedCount": "{{count}} 個權限不足",
"AlreadyTransferringCount": "{{count}} 個正在轉移中",
"ServerNotFoundCount": "{{count}} 個未找到",
"SameOwnerCount": "{{count}} 個已屬於目標使用者",
"AgentTooOldCount": "{{count}} 個 agent 版本過舊(需升級)",
"StatusPending": "待處理",
"StatusVerified": "已驗證",
"StatusFailed": "已失敗",
"StatusTimeout": "已逾時",
"StatusCancelled": "已取消"
},
"EnableMCP": "啟用 MCP 端點(預設關閉;啟用前請先檢查 API token 範圍與伺服器允許清單)",
"ApiTokens": "API Token",
"CreateApiToken": "建立 API token",
"CreateApiTokenDescription": "Token 會代表你對 MCP 與外部用戶端進行驗證,其權限不會超過你本身的權限。",
"ConfirmDeleteApiToken": "撤銷 API token「{{name}}」?此操作無法復原。",
"ApiTokenRevoked": "API token 已撤銷。",
"ApiTokenCreated": "API token 已建立",
"ApiTokenRevealOnce": "請立即複製此 token,之後將不再顯示。",
"ApiTokenStoreSafely": "請將此 token 視為密碼。任何持有者都能在其範圍內代表你操作。",
"ApiTokenServers": "伺服器",
"ApiTokenAllServers": "全部允許",
"ApiTokenNever": "永不",
"ApiTokenExpiresAt": "到期時間",
"ApiTokenLastUsed": "最後使用",
"ApiTokenServerIDs": "限制伺服器 ID(選填)",
"ApiTokenServerIDsPlaceholder": "以逗號分隔,例如 1,2,3",
"ApiTokenExpiresInDays": "有效天數(0 = 永不過期)",
"ApiTokenScopeRequired": "至少需要一個範圍。",
"ApiTokenServersInvalid": "伺服器 ID 必須為正整數。",
"ApiTokenExpiryInvalid": "天數必須介於 0 到 3650 之間。",
"NameRequired": "名稱為必填。",
"Revoke": "撤銷",
"Copy": "複製",
"Copied": "已複製到剪貼簿",
"Scopes": "範圍",
"InvalidUsernameOrPassword": "使用者名稱或密碼無效",
"NetworkError": "網路錯誤"
}
+12
View File
@@ -23,9 +23,12 @@ import ServerPage from "./routes/server"
import ServerGroupPage from "./routes/server-group"
import ServicePage from "./routes/service"
import SettingsPage from "./routes/settings"
import TransferPage from "./routes/transfer"
import UserPage from "./routes/user"
import WAFPage from "./routes/waf"
import DomainPage from "./routes/domain"
import ApiTokensPage from "./routes/api-tokens"
const router = createBrowserRouter([
{
path: "/dashboard",
@@ -143,8 +146,17 @@ const router = createBrowserRouter([
path: "/dashboard/domain",
element: <DomainPage />,
},
{
path: "/dashboard/settings/api-tokens",
element: <ApiTokensPage />,
},
{
path: "/dashboard/transfer",
element: <TransferPage />,
},
],
},
])
createRoot(document.getElementById("root")!).render(<RouterProvider router={router} />)
+5 -5
View File
@@ -14,8 +14,9 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table"
import { selectableTableFeatures } from "@/lib/table"
import { ModelAlertRule, triggerModes } from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { ColumnDef, flexRender, useTable } from "@tanstack/react-table"
import { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
@@ -37,7 +38,7 @@ export default function AlertRulePage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
const columns: ColumnDef<ModelAlertRule>[] = [
const columns: ColumnDef<typeof selectableTableFeatures, ModelAlertRule>[] = [
{
id: "select",
header: ({ table }) => (
@@ -57,7 +58,6 @@ export default function AlertRulePage() {
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
@@ -131,10 +131,10 @@ export default function AlertRulePage() {
return data ?? []
}, [data])
const table = useReactTable({
const table = useTable({
features: selectableTableFeatures,
data: dataCache,
columns,
getCoreRowModel: getCoreRowModel(),
})
const selectedRows = table.getSelectedRowModel().rows
+350
View File
@@ -0,0 +1,350 @@
import {
ApiTokenCreateResponse,
ApiTokenView,
SCOPE_OPTIONS,
createApiToken,
deleteApiToken,
listApiTokens,
parseExpiresInDaysInput,
parseServerIDsInput,
} from "@/api/api-tokens"
import { SettingsTab } from "@/components/settings-tab"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { useAuth } from "@/hooks/useAuth"
import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import useSWR from "swr"
export default function ApiTokensPage() {
const { t } = useTranslation()
const { profile } = useAuth()
const isAdmin = profile?.role === 0
const { data, mutate, isLoading, error } = useSWR<ApiTokenView[]>(
"/api/v1/api-tokens",
listApiTokens,
)
useEffect(() => {
if (!error) return
toast(t("Error"), {
description: t("Results.ErrorFetchingResource", {
error: (error as Error)?.message ?? String(error),
}),
})
}, [error, t])
const [createOpen, setCreateOpen] = useState(false)
const [revealed, setRevealed] = useState<ApiTokenCreateResponse | null>(null)
const handleDelete = async (id: number, name: string) => {
if (!window.confirm(t("ConfirmDeleteApiToken", { name }))) return
try {
await deleteApiToken(id)
toast(t("ApiTokenRevoked"))
await mutate()
} catch (e: any) {
toast(t("Error"), { description: e.message })
}
}
return (
<div className="px-3">
<SettingsTab className="mt-6 w-full" />
<div className="flex mt-4 mb-4 items-center justify-between">
<h2 className="text-lg font-semibold">{t("ApiTokens")}</h2>
<Button onClick={() => setCreateOpen(true)}>{t("CreateApiToken")}</Button>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>{t("Name")}</TableHead>
<TableHead>{t("Scopes")}</TableHead>
<TableHead>{t("ApiTokenServers")}</TableHead>
<TableHead>{t("ApiTokenExpiresAt")}</TableHead>
<TableHead>{t("ApiTokenLastUsed")}</TableHead>
<TableHead>{t("Actions")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={6} className="h-24 text-center">
{t("Loading")}...
</TableCell>
</TableRow>
) : !data || data.length === 0 ? (
<TableRow>
<TableCell
colSpan={6}
className="h-24 text-center text-muted-foreground"
>
{t("NoResults")}
</TableCell>
</TableRow>
) : (
data.map((tok) => (
<TableRow key={tok.id}>
<TableCell>{tok.name}</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{(tok.scopes ?? []).map((s) => (
<span
key={s}
className="rounded bg-secondary px-1.5 py-0.5 text-xs"
>
{s}
</span>
))}
</div>
</TableCell>
<TableCell className="text-xs">
{tok.server_ids?.length
? tok.server_ids.join(", ")
: t("ApiTokenAllServers")}
</TableCell>
<TableCell className="text-xs">
{tok.expires_at
? new Date(tok.expires_at).toLocaleString()
: t("ApiTokenNever")}
</TableCell>
<TableCell className="text-xs">
{tok.last_used_at
? `${new Date(tok.last_used_at).toLocaleString()} (${tok.last_used_ip ?? "?"})`
: t("ApiTokenNever")}
</TableCell>
<TableCell>
<Button
variant="destructive"
size="sm"
onClick={() => handleDelete(tok.id, tok.name)}
>
{t("Revoke")}
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
<CreateApiTokenDialog
open={createOpen}
onOpenChange={setCreateOpen}
isAdmin={isAdmin}
onCreated={(res) => {
setRevealed(res)
mutate()
}}
/>
<RevealedTokenDialog token={revealed} onClose={() => setRevealed(null)} />
</div>
)
}
function CreateApiTokenDialog({
open,
onOpenChange,
isAdmin,
onCreated,
}: {
open: boolean
onOpenChange: (v: boolean) => void
isAdmin: boolean
onCreated: (res: ApiTokenCreateResponse) => void
}) {
const { t } = useTranslation()
const [name, setName] = useState("")
const [scopes, setScopes] = useState<string[]>([])
const [serverIDs, setServerIDs] = useState("")
const [expiresInDays, setExpiresInDays] = useState<string>("90")
const [submitting, setSubmitting] = useState(false)
const toggleScope = (s: string) => {
setScopes((cur) => (cur.includes(s) ? cur.filter((x) => x !== s) : [...cur, s]))
}
const submit = async () => {
if (!name.trim()) {
toast(t("Error"), { description: t("NameRequired") })
return
}
if (scopes.length === 0) {
toast(t("Error"), { description: t("ApiTokenScopeRequired") })
return
}
const parsedRes = parseServerIDsInput(serverIDs)
if (!parsedRes.ok) {
toast(t("Error"), { description: t("ApiTokenServersInvalid") })
return
}
const parsedServers = parsedRes.value
const expRes = parseExpiresInDaysInput(expiresInDays)
if (!expRes.ok) {
toast(t("Error"), { description: t("ApiTokenExpiryInvalid") })
return
}
const expDays = expRes.value
setSubmitting(true)
try {
const res = await createApiToken({
name: name.trim(),
scopes,
server_ids: parsedServers,
expires_in_days: expDays,
})
onCreated(res)
onOpenChange(false)
setName("")
setScopes([])
setServerIDs("")
setExpiresInDays("90")
} catch (e: any) {
toast(t("Error"), { description: e.message })
} finally {
setSubmitting(false)
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{t("CreateApiToken")}</DialogTitle>
<DialogDescription>{t("CreateApiTokenDescription")}</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-2">
<div className="grid gap-2">
<Label htmlFor="name">{t("Name")}</Label>
<Input id="name" value={name} onChange={(e) => setName(e.target.value)} />
</div>
<div className="grid gap-2">
<Label>{t("Scopes")}</Label>
<div className="max-h-72 space-y-2 overflow-y-auto pr-2">
{SCOPE_OPTIONS.map((s) => {
const adminOnly =
s.value === "nezha:*" || s.value === "nezha:admin:*"
const disabled = adminOnly && !isAdmin
return (
<label
key={s.value}
className={`flex items-start gap-2 text-sm ${disabled ? "opacity-40" : ""}`}
>
<Checkbox
checked={scopes.includes(s.value)}
onCheckedChange={() =>
!disabled && toggleScope(s.value)
}
disabled={disabled}
/>
<div className="flex flex-col">
<span className="font-mono text-xs">{s.value}</span>
<span className="text-xs text-muted-foreground">
{s.desc}
</span>
</div>
</label>
)
})}
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="server-ids">{t("ApiTokenServerIDs")}</Label>
<Input
id="server-ids"
placeholder={t("ApiTokenServerIDsPlaceholder")}
value={serverIDs}
onChange={(e) => setServerIDs(e.target.value)}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="expires">{t("ApiTokenExpiresInDays")}</Label>
<Input
id="expires"
type="number"
min={0}
max={3650}
value={expiresInDays}
onChange={(e) => setExpiresInDays(e.target.value)}
/>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">{t("Cancel")}</Button>
</DialogClose>
<Button onClick={submit} disabled={submitting}>
{submitting ? t("Loading") + "..." : t("CreateApiToken")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
function RevealedTokenDialog({
token,
onClose,
}: {
token: ApiTokenCreateResponse | null
onClose: () => void
}) {
const { t } = useTranslation()
return (
<Dialog open={!!token} onOpenChange={(v) => !v && onClose()}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{t("ApiTokenCreated")}</DialogTitle>
<DialogDescription>{t("ApiTokenRevealOnce")}</DialogDescription>
</DialogHeader>
{token && (
<div className="space-y-3">
<div className="rounded border border-amber-500/40 bg-amber-100 dark:bg-amber-950/40 p-3 text-sm">
{t("ApiTokenStoreSafely")}
</div>
<code className="block break-all rounded bg-muted p-3 font-mono text-xs">
{token.token}
</code>
<Button
variant="secondary"
size="sm"
onClick={async () => {
await navigator.clipboard.writeText(token.token)
toast(t("Copied"))
}}
>
{t("Copy")}
</Button>
</div>
)}
<DialogFooter>
<Button onClick={onClose}>{t("Done")}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
+5 -5
View File
@@ -14,9 +14,10 @@ import {
TableRow,
} from "@/components/ui/table"
import { IconButton } from "@/components/xui/icon-button"
import { selectableTableFeatures } from "@/lib/table"
import { ModelCron } from "@/types"
import { cronTypes } from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { ColumnDef, flexRender, useTable } from "@tanstack/react-table"
import { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
@@ -36,7 +37,7 @@ export default function CronPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
const columns: ColumnDef<ModelCron>[] = [
const columns: ColumnDef<typeof selectableTableFeatures, ModelCron>[] = [
{
id: "select",
header: ({ table }) => (
@@ -56,7 +57,6 @@ export default function CronPage() {
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
@@ -199,10 +199,10 @@ export default function CronPage() {
return data ?? []
}, [data])
const table = useReactTable({
const table = useTable({
features: selectableTableFeatures,
data: dataCache,
columns,
getCoreRowModel: getCoreRowModel(),
})
const selectedRows = table.getSelectedRowModel().rows
+5 -5
View File
@@ -12,8 +12,9 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table"
import { selectableTableFeatures } from "@/lib/table"
import { ModelDDNSProfile } from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { ColumnDef, flexRender, useTable } from "@tanstack/react-table"
import { useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
@@ -45,7 +46,7 @@ export default function DDNSPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
const columns: ColumnDef<ModelDDNSProfile>[] = [
const columns: ColumnDef<typeof selectableTableFeatures, ModelDDNSProfile>[] = [
{
id: "select",
header: ({ table }) => (
@@ -65,7 +66,6 @@ export default function DDNSPage() {
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
@@ -136,10 +136,10 @@ export default function DDNSPage() {
return data ?? []
}, [data])
const table = useReactTable({
const table = useTable({
features: selectableTableFeatures,
data: dataCache,
columns,
getCoreRowModel: getCoreRowModel(),
})
const selectedRows = table.getSelectedRowModel().rows
+5 -4
View File
@@ -8,7 +8,7 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { GitHubIcon } from "@/components/ui/icon"
import { OAuthProviderIcon } from "@/components/ui/icon"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import { useAuth } from "@/hooks/useAuth"
@@ -39,7 +39,7 @@ function Login() {
if (oauth2) {
loginOauth2()
}
}, [window.location.search])
}, [loginOauth2])
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
@@ -56,7 +56,7 @@ function Login() {
async function loginWith(provider: string) {
try {
const redirectUrl = await getOauth2RedirectURL(provider, Oauth2RequestType.LOGIN)
window.location.href = redirectUrl.redirect!
window.location.assign(redirectUrl.redirect!)
} catch (error: any) {
toast.error(error.message)
}
@@ -120,10 +120,11 @@ function Login() {
<div className="mt-3 flex flex-col gap-3">
{settingData?.config?.oauth2_providers?.map((p: string) => (
<Button
key={p}
className="w-full rounded-lg shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] bg-muted text-primary hover:bg-muted/80 hover:text-primary/80"
onClick={() => loginWith(p)}
>
{p === "GitHub" && <GitHubIcon className="size-4" />}
<OAuthProviderIcon provider={p} className="size-4" />
{p}
</Button>
))}
+9 -8
View File
@@ -12,8 +12,9 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table"
import { selectableTableFeatures } from "@/lib/table"
import { ModelNAT } from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { ColumnDef, flexRender, useTable } from "@tanstack/react-table"
import { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
@@ -33,7 +34,7 @@ export default function NATPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
const columns: ColumnDef<ModelNAT>[] = [
const columns: ColumnDef<typeof selectableTableFeatures, ModelNAT>[] = [
{
id: "select",
header: ({ table }) => (
@@ -53,7 +54,6 @@ export default function NATPage() {
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
@@ -119,20 +119,20 @@ export default function NATPage() {
return data ?? []
}, [data])
const table = useReactTable({
const table = useTable({
features: selectableTableFeatures,
data: dataCache,
columns,
getCoreRowModel: getCoreRowModel(),
})
const selectedRows = table.getSelectedRowModel().rows
return (
<div className="px-3">
<div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight"> {t("NATT")}</h1>
<div className="flex flex-col sm:flex-row sm:items-center justify-between w-full gap-4 mt-6 mb-4">
<h1 className="text-3xl font-bold tracking-tight shrink-0">{t("NATT")}</h1>
<HeaderButtonGroup
className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0"
className="flex items-center gap-2 flex-wrap shrink-0 sm:ml-auto"
delete={{
fn: deleteNAT,
id: selectedRows.map((r) => r.original.id!),
@@ -143,6 +143,7 @@ export default function NATPage() {
</HeaderButtonGroup>
</div>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
+10 -5
View File
@@ -13,13 +13,19 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table"
import { selectableTableFeatures } from "@/lib/table"
import { ModelNotificationGroupResponseItem } from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { ColumnDef, flexRender, useTable } from "@tanstack/react-table"
import { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import useSWR from "swr"
type NotificationGroupColumn = ColumnDef<
typeof selectableTableFeatures,
ModelNotificationGroupResponseItem
>
export default function NotificationGroupPage() {
const { t } = useTranslation()
const { data, mutate, error, isLoading } = useSWR<ModelNotificationGroupResponseItem[]>(
@@ -37,7 +43,7 @@ export default function NotificationGroupPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
const columns: ColumnDef<ModelNotificationGroupResponseItem>[] = [
const columns: NotificationGroupColumn[] = [
{
id: "select",
header: ({ table }) => (
@@ -57,7 +63,6 @@ export default function NotificationGroupPage() {
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
@@ -112,10 +117,10 @@ export default function NotificationGroupPage() {
return data ?? []
}, [data])
const table = useReactTable({
const table = useTable({
features: selectableTableFeatures,
data: dataCache,
columns,
getCoreRowModel: getCoreRowModel(),
})
const selectedRows = table.getSelectedRowModel().rows
+5 -5
View File
@@ -15,8 +15,9 @@ import {
TableRow,
} from "@/components/ui/table"
import { useNotification } from "@/hooks/useNotfication"
import { selectableTableFeatures } from "@/lib/table"
import { ModelNotification } from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { ColumnDef, flexRender, useTable } from "@tanstack/react-table"
import { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
@@ -40,7 +41,7 @@ export default function NotificationPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
const columns: ColumnDef<ModelNotification>[] = [
const columns: ColumnDef<typeof selectableTableFeatures, ModelNotification>[] = [
{
id: "select",
header: ({ table }) => (
@@ -60,7 +61,6 @@ export default function NotificationPage() {
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
@@ -127,10 +127,10 @@ export default function NotificationPage() {
return data ?? []
}, [data])
const table = useReactTable({
const table = useTable({
features: selectableTableFeatures,
data: dataCache,
columns,
getCoreRowModel: getCoreRowModel(),
})
const selectedRows = table.getSelectedRowModel().rows
+8 -5
View File
@@ -22,8 +22,12 @@ import {
TableRow,
} from "@/components/ui/table"
import { useAuth } from "@/hooks/useAuth"
import { selectableTableFeatures } from "@/lib/table"
import { GithubComNezhahqNezhaModelPaginatedResponseArrayModelOnlineUserModelOnlineUser, ModelOnlineUser } from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { ColumnDef, flexRender, useTable } from "@tanstack/react-table"
import { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { useSearchParams } from "react-router-dom"
@@ -55,7 +59,7 @@ export default function OnlineUserPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
let columns: ColumnDef<ModelOnlineUser>[] = [
let columns: ColumnDef<typeof selectableTableFeatures, ModelOnlineUser>[] = [
{
id: "select",
header: ({ table }) => (
@@ -75,7 +79,6 @@ export default function OnlineUserPage() {
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
@@ -128,10 +131,10 @@ export default function OnlineUserPage() {
return data?.data?.value ?? []
}, [data])
const table = useReactTable<ModelOnlineUser>({
const table = useTable({
features: selectableTableFeatures,
data: dataCache,
columns,
getCoreRowModel: getCoreRowModel(),
})
const selectedRows = table.getSelectedRowModel().rows
+11 -3
View File
@@ -4,6 +4,7 @@ import { ProfileCard } from "@/components/profile"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { OAuthProviderIcon } from "@/components/ui/icon"
import { useMainStore } from "@/hooks/useMainStore"
import { useMediaQuery } from "@/hooks/useMediaQuery"
import { useServer } from "@/hooks/useServer"
@@ -26,12 +27,12 @@ export default function ProfilePage() {
})
window.history.replaceState({}, document.title, window.location.pathname)
}
}, [window.location.search])
}, [setProfile])
const bindO2 = async (provider: string) => {
try {
const redirectUrl = await getOauth2RedirectURL(provider, Oauth2RequestType.BIND)
window.location.href = redirectUrl.redirect!
window.location.assign(redirectUrl.redirect!)
} catch (error: any) {
toast.error(error.message)
}
@@ -107,8 +108,15 @@ export default function ProfilePage() {
</CardHeader>
<CardContent className="text-lg font-semibold">
{settingData?.config?.oauth2_providers?.map((provider) => (
<div className="flex justify-between items-center flex-wrap gap-2">
<div
key={provider}
className="flex justify-between items-center flex-wrap gap-2"
>
<section className="flex gap-2 items-center">
<OAuthProviderIcon
provider={provider}
className="size-4 text-muted-foreground"
/>
<p>{provider}: </p>
{profile.oauth2_bind?.[provider.toLowerCase()] && (
<p className=" bg-muted px-1.5 py-0.5 text-sm rounded-full">
+29 -9
View File
@@ -1,16 +1,36 @@
import { useAuth } from "@/hooks/useAuth"
import { Navigate } from "react-router-dom"
import { Navigate, useLocation } from "react-router-dom"
const LOGIN_PATH = "/dashboard/login"
export const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
const { profile } = useAuth()
const { profile, loading } = useAuth()
const { pathname } = useLocation()
if (!profile && window.location.pathname !== "/dashboard/login") {
return (
<>
<Navigate to="/dashboard/login" />
{children}
</>
)
// While AuthProvider's initial getProfile() round-trip is in flight we
// can't yet decide between "render protected subtree" and "redirect to
// login". Two key invariants:
// - For the login page itself, render children so the user can log in
// without waiting for an unrelated /api/v1/profile probe.
// - For every other /dashboard/* path, do NOT mount children — the
// protected subtree (Root + Outlet + page) would fire authenticated
// SWR fetches like /api/v1/setting before auth is even confirmed,
// and a subsequent redirect would leave that work unobserved. A
// blank render during the (short) probe avoids that wasted round-trip
// and the flash of protected UI before redirect.
if (loading) {
if (pathname === LOGIN_PATH) {
return children
}
return null
}
if (!profile && pathname !== LOGIN_PATH) {
// `replace` keeps the unauthenticated URL out of history. Crucially do
// NOT render `children` alongside Navigate: that would mount the
// protected subtree for one paint and fire authenticated requests we
// are about to redirect away from.
return <Navigate to={LOGIN_PATH} replace />
}
return children
+6 -4
View File
@@ -22,6 +22,12 @@ export default function Root() {
}
}, [settingData?.config?.custom_code_dashboard])
useEffect(() => {
if (settingData?.config?.language && !localStorage.getItem("language")) {
i18n.changeLanguage(settingData?.config?.language)
}
}, [settingData?.config?.language])
if (error) {
throw error
}
@@ -30,10 +36,6 @@ export default function Root() {
return null
}
if (settingData?.config?.language && !localStorage.getItem("language")) {
i18n.changeLanguage(settingData?.config?.language)
}
return (
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
<section
+5 -5
View File
@@ -13,8 +13,9 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table"
import { selectableTableFeatures } from "@/lib/table"
import { ModelServerGroupResponseItem } from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { ColumnDef, flexRender, useTable } from "@tanstack/react-table"
import { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
@@ -37,7 +38,7 @@ export default function ServerGroupPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
const columns: ColumnDef<ModelServerGroupResponseItem>[] = [
const columns: ColumnDef<typeof selectableTableFeatures, ModelServerGroupResponseItem>[] = [
{
id: "select",
header: ({ table }) => (
@@ -57,7 +58,6 @@ export default function ServerGroupPage() {
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
@@ -112,10 +112,10 @@ export default function ServerGroupPage() {
return data ?? []
}, [data])
const table = useReactTable({
const table = useTable({
features: selectableTableFeatures,
data: dataCache,
columns,
getCoreRowModel: getCoreRowModel(),
})
const selectedRows = table.getSelectedRowModel().rows
+36 -8
View File
@@ -20,9 +20,10 @@ import {
} from "@/components/ui/table"
import { IconButton } from "@/components/xui/icon-button"
import { useServer } from "@/hooks/useServer"
import { selectableTableFeatures } from "@/lib/table"
import { joinIP } from "@/lib/utils"
import { ModelServerTaskResponse, ModelServer as Server } from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { ColumnDef, flexRender, useTable } from "@tanstack/react-table"
import { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
@@ -43,7 +44,7 @@ export default function ServerPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
const columns = useMemo<ColumnDef<Server>[]>(
const columns = useMemo<ColumnDef<typeof selectableTableFeatures, Server>[]>(
() => [
{
id: "select",
@@ -64,7 +65,6 @@ export default function ServerPage() {
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
@@ -81,6 +81,33 @@ export default function ServerPage() {
return <div className="max-w-24 whitespace-normal break-words">{s.name}</div>
},
},
{
id: "owner",
header: t("Owner"),
accessorFn: (row) => {
if (!row.owner) return ""
if (row.owner.id === 0) return t("GlobalAgent")
return row.owner.username || t("UnknownUser", { id: row.owner.id })
},
cell: ({ row }) => {
const owner = row.original.owner
if (!owner) {
return <span className="text-muted-foreground">-</span>
}
if (owner.id === 0) {
return <span>{t("GlobalAgent")}</span>
}
const label = owner.username || t("UnknownUser", { id: owner.id })
return (
<div
className="max-w-32 whitespace-normal break-words"
title={`uid=${owner.id}`}
>
{label}
</div>
)
},
},
{
header: t("Group"),
accessorKey: "groups",
@@ -161,20 +188,20 @@ export default function ServerPage() {
return data ?? []
}, [data])
const table = useReactTable({
const table = useTable({
features: selectableTableFeatures,
data: dataCache,
columns,
getCoreRowModel: getCoreRowModel(),
})
const selectedRows = table.getSelectedRowModel().rows
return (
<div className="px-3">
<div className="flex mt-6 mb-4">
<h1 className="text-3xl font-bold tracking-tight">{t("Server")}</h1>
<div className="flex flex-col sm:flex-row sm:items-center justify-between w-full gap-4 mt-6 mb-4">
<h1 className="text-3xl font-bold tracking-tight shrink-0">{t("Server")}</h1>
<HeaderButtonGroup
className="flex-2 flex ml-auto gap-2"
className="flex items-center gap-2 flex-wrap shrink-0 sm:ml-auto"
delete={{
fn: deleteServer,
id: selectedRows.map((r) => r.original.id).filter(Boolean) as number[],
@@ -227,6 +254,7 @@ export default function ServerPage() {
<InstallCommandsMenu className="shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] bg-blue-700 text-white hover:bg-blue-600 dark:hover:bg-blue-800 rounded-lg" />
</HeaderButtonGroup>
</div>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
+5 -5
View File
@@ -12,9 +12,10 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table"
import { selectableTableFeatures } from "@/lib/table"
import { ModelService as Service } from "@/types"
import { serviceTypes } from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { ColumnDef, flexRender, useTable } from "@tanstack/react-table"
import { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
@@ -34,7 +35,7 @@ export default function ServicePage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
const columns: ColumnDef<Service>[] = [
const columns: ColumnDef<typeof selectableTableFeatures, Service>[] = [
{
id: "select",
header: ({ table }) => (
@@ -54,7 +55,6 @@ export default function ServicePage() {
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
@@ -165,10 +165,10 @@ export default function ServicePage() {
return data ?? []
}, [data])
const table = useReactTable({
const table = useTable({
features: selectableTableFeatures,
data: dataCache,
columns,
getCoreRowModel: getCoreRowModel(),
})
const selectedRows = table.getSelectedRowModel().rows
+84 -28
View File
@@ -7,6 +7,7 @@ import { Combobox } from "@/components/ui/combobox"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
@@ -25,13 +26,13 @@ import { Textarea } from "@/components/ui/textarea"
import { useAuth } from "@/hooks/useAuth"
import { useNotification } from "@/hooks/useNotfication"
import useSetting from "@/hooks/useSetting"
import { asOptionalField } from "@/lib/utils"
import { asOptionalField, safeExternalHref } from "@/lib/utils"
import { nezhaLang, settingCoverageTypes } from "@/types"
import { zodResolver } from "@hookform/resolvers/zod"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import { Navigate } from "react-router-dom"
import { toast } from "sonner"
import { z } from "zod"
@@ -44,6 +45,8 @@ const settingFormSchema = z.object({
language: z.string().min(2),
user_template: z.string().min(1),
install_host: asOptionalField(z.string()),
dashboard_host: asOptionalField(z.string()),
reserved_hosts: asOptionalField(z.string()),
custom_code: asOptionalField(z.string()),
custom_code_dashboard: asOptionalField(z.string()),
web_real_ip_header: asOptionalField(z.string()),
@@ -53,6 +56,7 @@ const settingFormSchema = z.object({
enable_ip_change_notification: asOptionalField(z.boolean()),
enable_plain_ip_in_notification: asOptionalField(z.boolean()),
custom_logo: asOptionalField(z.string()),
custom_description: asOptionalField(z.string()),
custom_links: asOptionalField(z.string()),
background_image_day: asOptionalField(z.string()),
@@ -65,14 +69,17 @@ const settingFormSchema = z.object({
admin_email: asOptionalField(z.string()),
domain_expiry_notification_days: asOptionalField(z.string()),
server_expiry_notification_days: asOptionalField(z.string()),
expiry_notification_group_id: z.coerce.number().int().min(0),
expiry_notification_group_id: asOptionalField(z.coerce.number().int().min(0)),
enable_mcp: asOptionalField(z.boolean()),
})
export default function SettingsPage() {
const { t, i18n } = useTranslation()
const { data: config, mutate } = useSetting()
const { profile } = useAuth()
const navigate = useNavigate()
const { profile, loading: authLoading } = useAuth()
const { notifierGroup } = useNotification()
const ngroupList = notifierGroup?.map((ng) => ({
@@ -82,12 +89,10 @@ export default function SettingsPage() {
const isAdmin = profile?.role === 0
if (!isAdmin) {
navigate("/dashboard/settings/online-user")
}
const form = useForm({
// 所有 hooks 必须在条件 return 之前调用,否则违反 rules-of-hooks。
const form = useForm<z.infer<typeof settingFormSchema>>({
resolver: zodResolver(settingFormSchema) as any,
defaultValues: config
? {
...config.config,
@@ -117,6 +122,13 @@ export default function SettingsPage() {
}
}, [config?.config, form])
if (authLoading) {
return null
}
if (!isAdmin) {
return <Navigate to="/dashboard/settings/api-tokens" replace />
}
const onSubmit = async (values: any) => {
try {
await updateSettings(values)
@@ -129,19 +141,19 @@ export default function SettingsPage() {
}),
})
return
} finally {
}
if (values.language != i18n.language) {
i18n.changeLanguage(values.language)
}
toast(t("Success"))
}
}
return (
<div className="px-3">
<SettingsTab className="mt-6 mb-4 w-full" />
<div>
<Form {...form}>
<Form {...(form as any)}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 my-2">
<FormField
control={form.control}
@@ -390,14 +402,24 @@ export default function SettingsPage() {
</div>
</SelectItem>
<div className="px-8 py-1">
{safeExternalHref(
template.repository,
) ? (
<a
href={template.repository}
href={safeExternalHref(
template.repository,
)}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-600 hover:text-blue-800 hover:underline"
>
{template.repository}
</a>
) : (
<span className="text-sm text-muted-foreground">
{template.repository}
</span>
)}
</div>
</div>
))}
@@ -459,6 +481,34 @@ export default function SettingsPage() {
</FormItem>
)}
/>
<FormField
control={form.control}
name="dashboard_host"
render={({ field }) => (
<FormItem>
<FormLabel>{t("DashboardHost")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>{t("DashboardHostHint")}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="reserved_hosts"
render={({ field }) => (
<FormItem>
<FormLabel>{t("ReservedHosts")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>{t("ReservedHostsHint")}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="tls"
@@ -512,16 +562,10 @@ export default function SettingsPage() {
checked={field.value == "NZ::Use-Peer-IP"}
className="ml-2"
onCheckedChange={(checked) => {
if (checked) {
field.disabled = true
form.setValue(
"web_real_ip_header",
"NZ::Use-Peer-IP",
checked ? "NZ::Use-Peer-IP" : "",
)
} else {
field.disabled = false
form.setValue("web_real_ip_header", "")
}
}}
/>
<FormLabel className="font-normal ml-2">
@@ -551,16 +595,10 @@ export default function SettingsPage() {
checked={field.value == "NZ::Use-Peer-IP"}
className="ml-2"
onCheckedChange={(checked) => {
if (checked) {
field.disabled = true
form.setValue(
"agent_real_ip_header",
"NZ::Use-Peer-IP",
checked ? "NZ::Use-Peer-IP" : "",
)
} else {
field.disabled = false
form.setValue("agent_real_ip_header", "")
}
}}
/>
<FormLabel className="font-normal ml-2">
@@ -749,6 +787,24 @@ export default function SettingsPage() {
</FormItem>
)}
/>
<FormField
control={form.control}
name="enable_mcp"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<div className="flex items-center gap-2">
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">{t("EnableMCP")}</Label>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">{t("Confirm")}</Button>
</form>
</Form>
+257
View File
@@ -0,0 +1,257 @@
import { swrFetcher } from "@/api/api"
import { cancelServerTransfer, retryServerTransfer } from "@/api/transfer"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { useAuth } from "@/hooks/useAuth"
import {
ModelServerTransfer,
ModelServerTransferStatus,
ModelServerTransferStatusCancelled,
ModelServerTransferStatusFailed,
ModelServerTransferStatusPending,
ModelServerTransferStatusTimeout,
ModelServerTransferStatusVerified,
} from "@/types"
import { useCallback, useEffect, useMemo, useRef } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import useSWR from "swr"
// Map status enum to a label key + a Badge variant. Centralised here so the
// table cell and any future drawer agree. Verified is "secondary" because
// the row is no longer actionable; Failed/Timeout use "destructive" to draw
// the operator's attention. Cancelled is "outline" — soft signal, expected.
const statusMeta: Record<
ModelServerTransferStatus,
{ key: string; variant: "default" | "secondary" | "destructive" | "outline" }
> = {
[ModelServerTransferStatusPending]: { key: "Transfer.StatusPending", variant: "default" },
[ModelServerTransferStatusVerified]: { key: "Transfer.StatusVerified", variant: "secondary" },
[ModelServerTransferStatusFailed]: { key: "Transfer.StatusFailed", variant: "destructive" },
[ModelServerTransferStatusTimeout]: { key: "Transfer.StatusTimeout", variant: "destructive" },
[ModelServerTransferStatusCancelled]: { key: "Transfer.StatusCancelled", variant: "outline" },
}
export default function TransferPage() {
const { t } = useTranslation()
const { profile } = useAuth()
const isAdmin = profile?.role === 0
const callerID = profile?.id
const { data, mutate, error } = useSWR<ModelServerTransfer[]>("/api/v1/transfer", swrFetcher)
useEffect(() => {
if (error) {
toast(t("Error"), {
description: t("Results.UnExpectedError", { error: error.message }),
})
}
}, [error, t])
// Live updates: subscribe to /ws/transfer and patch the SWR cache on each
// pushed event. We don't refetch on every event because the WS payload IS
// the latest row — refetching would just cost a round trip.
//
// Reconnect contract: the original implementation opened the socket once
// and never reconnected, so a dashboard restart, deploy, or transient
// network blip silently froze this view — rows stayed Pending forever
// even after the agent had reconnected and the backend had transitioned
// them, and operators only discovered it on a hard refresh. Reconnect
// with capped exponential backoff (1s → 30s) and trigger a SWR revalidate
// on each open so any transitions that broadcast while we were offline
// are reconciled.
const wsRef = useRef<WebSocket | null>(null)
useEffect(() => {
let cancelled = false
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
let backoffMs = 1000
const maxBackoffMs = 30000
const connect = () => {
if (cancelled) return
const proto = window.location.protocol === "https:" ? "wss:" : "ws:"
const ws = new WebSocket(`${proto}//${window.location.host}/api/v1/ws/transfer`)
wsRef.current = ws
ws.onopen = () => {
backoffMs = 1000
// Backfill anything the broker fanned out while we were
// reconnecting — the broker drops events for full subscribers
// and there is no replay on the wire.
mutate()
}
ws.onmessage = (ev) => {
try {
const t: ModelServerTransfer = JSON.parse(ev.data)
mutate((prev) => {
const list = prev ?? []
const idx = list.findIndex((x) => x.id === t.id)
if (idx === -1) return [t, ...list]
const next = list.slice()
next[idx] = t
return next
}, false)
} catch (e) {
// Malformed payload — log and keep the socket open; a refetch
// on next mount will re-sync. Closing the WS would be louder
// than the actual problem.
console.error("transfer ws parse failed", e)
}
}
const scheduleReconnect = () => {
if (cancelled) return
if (reconnectTimer !== null) return
const delay = backoffMs
backoffMs = Math.min(backoffMs * 2, maxBackoffMs)
reconnectTimer = setTimeout(() => {
reconnectTimer = null
connect()
}, delay)
}
ws.onclose = scheduleReconnect
ws.onerror = () => {
// onerror fires before onclose and may not produce a close
// event in every browser when the handshake is rejected.
// Force-close so onclose's reconnect path runs deterministically.
ws.close()
}
}
connect()
return () => {
cancelled = true
if (reconnectTimer !== null) {
clearTimeout(reconnectTimer)
reconnectTimer = null
}
const ws = wsRef.current
wsRef.current = null
if (ws) {
// Detach handlers so the close we trigger here doesn't bounce
// into a reconnect attempt against an unmounted component.
ws.onopen = null
ws.onmessage = null
ws.onclose = null
ws.onerror = null
ws.close()
}
}
}, [mutate])
const onCancel = useCallback(
async (id: number) => {
try {
await cancelServerTransfer(id)
toast(t("Transfer.CancelRequested"))
mutate()
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e)
toast(t("Error"), { description: msg })
}
},
[mutate, t],
)
const onRetry = useCallback(
async (id: number) => {
try {
await retryServerTransfer(id)
toast(t("Transfer.RetryRequested"))
mutate()
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e)
toast(t("Error"), { description: msg })
}
},
[mutate, t],
)
const rows = useMemo(() => data ?? [], [data])
return (
<div className="px-3">
<h1 className="mt-6 text-2xl font-semibold">{t("Transfer.Title")}</h1>
<p className="text-sm text-muted-foreground mt-1 max-w-2xl">{t("Transfer.PageHint")}</p>
<Table className="mt-6">
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>{t("ServerID")}</TableHead>
<TableHead>{t("Transfer.From")}</TableHead>
<TableHead>{t("Transfer.To")}</TableHead>
<TableHead>{t("Transfer.Initiator")}</TableHead>
<TableHead>{t("Status")}</TableHead>
<TableHead>{t("Transfer.LastError")}</TableHead>
<TableHead>{t("CreatedAt")}</TableHead>
<TableHead>{t("Actions")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rows.length === 0 ? (
<TableRow>
<TableCell colSpan={9} className="h-24 text-center">
{t("NoResults")}
</TableCell>
</TableRow>
) : (
rows.map((row) => {
const meta = statusMeta[row.status]
const isPending = row.status === ModelServerTransferStatusPending
const isTerminalRevert =
row.status === ModelServerTransferStatusFailed ||
row.status === ModelServerTransferStatusTimeout ||
row.status === ModelServerTransferStatusCancelled
return (
<TableRow key={row.id}>
<TableCell>{row.id}</TableCell>
<TableCell>{row.server_id}</TableCell>
<TableCell>{row.from_user_id}</TableCell>
<TableCell>{row.to_user_id}</TableCell>
<TableCell>{row.initiator_id}</TableCell>
<TableCell>
<Badge variant={meta.variant}>{t(meta.key)}</Badge>
</TableCell>
<TableCell className="max-w-xs truncate" title={row.last_error}>
{row.last_error || "-"}
</TableCell>
<TableCell>
{new Date(row.created_at).toLocaleString()}
</TableCell>
<TableCell className="space-x-2">
{isPending &&
(isAdmin || row.from_user_id === callerID) && (
<Button
size="sm"
variant="outline"
onClick={() => onCancel(row.id)}
>
{t("Cancel")}
</Button>
)}
{isTerminalRevert && isAdmin && (
<Button
size="sm"
variant="outline"
onClick={() => onRetry(row.id)}
>
{t("Transfer.Retry")}
</Button>
)}
</TableCell>
</TableRow>
)
})
)}
</TableBody>
</Table>
</div>
)
}
+5 -5
View File
@@ -13,8 +13,9 @@ import {
TableRow,
} from "@/components/ui/table"
import { UserCard } from "@/components/user"
import { selectableTableFeatures } from "@/lib/table"
import { ModelUser } from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { ColumnDef, flexRender, useTable } from "@tanstack/react-table"
import { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
@@ -34,7 +35,7 @@ export default function UserPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
const columns: ColumnDef<ModelUser>[] = [
const columns: ColumnDef<typeof selectableTableFeatures, ModelUser>[] = [
{
id: "select",
header: ({ table }) => (
@@ -54,7 +55,6 @@ export default function UserPage() {
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
@@ -105,10 +105,10 @@ export default function UserPage() {
return data ?? []
}, [data])
const table = useReactTable({
const table = useTable({
features: selectableTableFeatures,
data: dataCache,
columns,
getCoreRowModel: getCoreRowModel(),
})
const selectedRows = table.getSelectedRowModel().rows
+5 -5
View File
@@ -22,13 +22,14 @@ import {
TableRow,
} from "@/components/ui/table"
import { useAuth } from "@/hooks/useAuth"
import { selectableTableFeatures } from "@/lib/table"
import {
GithubComNezhahqNezhaModelValueArrayModelWAFApiMock,
ModelWAFApiMock,
wafBlockIdentifiers,
wafBlockReasons,
} from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { ColumnDef, flexRender, useTable } from "@tanstack/react-table"
import { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { useSearchParams } from "react-router-dom"
@@ -61,7 +62,7 @@ export default function WAFPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
let columns: ColumnDef<ModelWAFApiMock>[] = [
let columns: ColumnDef<typeof selectableTableFeatures, ModelWAFApiMock>[] = [
{
id: "select",
header: ({ table }) => (
@@ -81,7 +82,6 @@ export default function WAFPage() {
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
@@ -147,10 +147,10 @@ export default function WAFPage() {
return data?.value ?? []
}, [data])
const table = useReactTable({
const table = useTable({
features: selectableTableFeatures,
data: dataCache,
columns,
getCoreRowModel: getCoreRowModel(),
})
const selectedRows = table.getSelectedRowModel().rows
+30
View File
@@ -0,0 +1,30 @@
import { expect, test } from "vitest"
import { parseExpiresInDaysInput } from "../api/api-tokens"
test("parseExpiresInDaysInput treats blank as 'never expires' (undefined)", () => {
expect(parseExpiresInDaysInput("")).toEqual({ ok: true, value: undefined })
expect(parseExpiresInDaysInput(" ")).toEqual({ ok: true, value: undefined })
})
test("parseExpiresInDaysInput accepts whole-number days in range", () => {
expect(parseExpiresInDaysInput("30")).toEqual({ ok: true, value: 30 })
expect(parseExpiresInDaysInput(" 3650 ")).toEqual({ ok: true, value: 3650 })
})
test("parseExpiresInDaysInput maps 0 to undefined (never expires)", () => {
expect(parseExpiresInDaysInput("0")).toEqual({ ok: true, value: undefined })
})
// Backend model field is `ExpiresInDays int` (nezha/model/api_token.go); a
// fractional value would fail JSON binding server-side, so the UI must reject
// it locally rather than send 1.5 and surface a confusing backend error.
test("parseExpiresInDaysInput rejects fractional days", () => {
expect(parseExpiresInDaysInput("1.5").ok).toBe(false)
})
test("parseExpiresInDaysInput rejects out-of-range and non-numeric input", () => {
expect(parseExpiresInDaysInput("-1").ok).toBe(false)
expect(parseExpiresInDaysInput("3651").ok).toBe(false)
expect(parseExpiresInDaysInput("abc").ok).toBe(false)
})
+26
View File
@@ -0,0 +1,26 @@
import { expect, test } from "vitest"
import { parseServerIDsInput } from "../api/api-tokens"
test("parseServerIDsInput returns undefined for empty input", () => {
expect(parseServerIDsInput("")).toEqual({ ok: true, value: undefined })
expect(parseServerIDsInput(" ")).toEqual({ ok: true, value: undefined })
})
test("parseServerIDsInput parses valid comma-separated positive ints", () => {
expect(parseServerIDsInput("1,2,3")).toEqual({ ok: true, value: [1, 2, 3] })
expect(parseServerIDsInput(" 10 , 11 ")).toEqual({ ok: true, value: [10, 11] })
})
test("parseServerIDsInput rejects any non-numeric token rather than silently dropping it", () => {
// 历史 UI 把 "1,abc" 静默裁剪为 [1] 再上送,等价于把"输入完整接受"骗给用户。
// 这条契约要求:任一片段非法 → 整次解析失败,前端必须报错而不是吞掉。
const r = parseServerIDsInput("1,abc")
expect(r.ok).toBe(false)
})
test("parseServerIDsInput rejects non-positive ids", () => {
expect(parseServerIDsInput("0").ok).toBe(false)
expect(parseServerIDsInput("-3").ok).toBe(false)
expect(parseServerIDsInput("1.5").ok).toBe(false)
})
+138
View File
@@ -0,0 +1,138 @@
import { afterEach, beforeEach, expect, test, vi } from "vitest"
import { createApiToken, deleteApiToken, listApiTokens } from "../api/api-tokens"
const realFetch = globalThis.fetch
function mockFetch(payload: unknown, ok = true, success = true) {
globalThis.fetch = vi.fn(async () => {
return new Response(
JSON.stringify({ success, error: success ? "" : "boom", data: payload }),
{
status: ok ? 200 : 500,
headers: { "Content-Type": "application/json" },
},
)
}) as unknown as typeof fetch
}
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
globalThis.fetch = realFetch
vi.restoreAllMocks()
})
test("listApiTokens GETs /api/v1/api-tokens and returns parsed array", async () => {
const calls: Array<{ url: string; method: string }> = []
globalThis.fetch = vi.fn(async (input: any, init?: any) => {
calls.push({ url: String(input), method: String(init?.method ?? "GET") })
return new Response(
JSON.stringify({
success: true,
data: [
{
id: 1,
name: "claude",
scopes: ["nezha:server:read"],
created_at: "2025-01-01T00:00:00Z",
},
],
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
)
}) as unknown as typeof fetch
const got = await listApiTokens()
expect(calls).toHaveLength(1)
expect(calls[0].method).toBe("GET")
expect(calls[0].url).toContain("/api/v1/api-tokens")
expect(got).toHaveLength(1)
expect(got[0].name).toBe("claude")
expect(got[0].scopes).toContain("nezha:server:read")
})
test("createApiToken POSTs /api/v1/api-tokens and serializes scopes / server_ids / expires_in_days", async () => {
let captured: { url: string; method: string; body: any } | null = null
globalThis.fetch = vi.fn(async (input: any, init?: any) => {
captured = {
url: String(input),
method: String(init?.method ?? ""),
body: init?.body ? JSON.parse(init.body as string) : null,
}
return new Response(
JSON.stringify({
success: true,
data: {
id: 2,
name: "x",
token: "nzp_FAKEABC",
scopes: ["nezha:server:read", "nezha:server:write"],
server_ids: [10, 11],
expires_at: "2026-01-01T00:00:00Z",
},
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
)
}) as unknown as typeof fetch
const res = await createApiToken({
name: "x",
scopes: ["nezha:server:read", "nezha:server:write"],
server_ids: [10, 11],
expires_in_days: 30,
})
expect(res.token).toBe("nzp_FAKEABC")
expect(captured).not.toBeNull()
expect(captured!.method).toBe("POST")
expect(captured!.url).toContain("/api/v1/api-tokens")
expect(captured!.body.name).toBe("x")
expect(captured!.body.scopes).toEqual(["nezha:server:read", "nezha:server:write"])
expect(captured!.body.server_ids).toEqual([10, 11])
expect(captured!.body.expires_in_days).toBe(30)
})
test("createApiToken surfaces server error via thrown Error", async () => {
mockFetch(null, true, false)
await expect(createApiToken({ name: "x", scopes: ["nezha:server:read"] })).rejects.toThrow(
"boom",
)
})
test("listApiTokens normalizes null scopes to an empty array so the table cannot crash", async () => {
// Backend APIToken.Scopes() returns nil for ScopesCSV=="" which JSON-encodes
// as null; migrated/legacy/hand-edited rows hit this. Without normalization
// the list page does tok.scopes.map(...) on null and the whole page crashes.
globalThis.fetch = vi.fn(async () => {
return new Response(
JSON.stringify({
success: true,
data: [{ id: 1, name: "legacy", scopes: null, created_at: "2025-01-01T00:00:00Z" }],
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
)
}) as unknown as typeof fetch
const got = await listApiTokens()
expect(Array.isArray(got[0].scopes)).toBe(true)
expect(got[0].scopes).toEqual([])
})
test("deleteApiToken DELETEs /api/v1/api-tokens/:id", async () => {
const calls: Array<{ url: string; method: string }> = []
globalThis.fetch = vi.fn(async (input: any, init?: any) => {
calls.push({ url: String(input), method: String(init?.method ?? "GET") })
return new Response(JSON.stringify({ success: true, data: null }), {
status: 200,
headers: { "Content-Type": "application/json" },
})
}) as unknown as typeof fetch
await deleteApiToken(42)
expect(calls).toHaveLength(1)
expect(calls[0].method).toBe("DELETE")
expect(calls[0].url).toContain("/api/v1/api-tokens/42")
})
+150
View File
@@ -0,0 +1,150 @@
import { act, render } from "@testing-library/react"
import { useEffect } from "react"
import { afterEach, beforeEach, expect, test, vi } from "vitest"
let profileStore: { id: number; role: number } | undefined
const setProfileSpy = vi.fn((p: any) => {
profileStore = p
})
vi.mock("./useMainStore", () => ({}))
vi.mock("@/hooks/useMainStore", () => ({
useMainStore: (selector: any) => selector({ profile: profileStore, setProfile: setProfileSpy }),
}))
vi.mock("react-i18next", () => ({
useTranslation: () => ({ t: (k: string) => k }),
}))
vi.mock("sonner", () => ({ toast: () => {} }))
const navigate = vi.fn()
vi.mock("react-router-dom", () => ({
useNavigate: () => navigate,
}))
// Initial getProfile() stays pending forever so loading can only be cleared
// by an explicit login/logout, which is exactly what these tests assert.
let initialProfilePromise: Promise<any>
let getProfileCall = 0
const loginRequest = vi.fn(async () => {})
vi.mock("@/api/user", () => ({
getProfile: vi.fn(() => {
getProfileCall++
if (getProfileCall === 1) return initialProfilePromise
return Promise.resolve({ id: 42, role: 0 })
}),
login: () => loginRequest(),
}))
beforeEach(() => {
profileStore = undefined
getProfileCall = 0
initialProfilePromise = new Promise<any>(() => {})
setProfileSpy.mockClear()
navigate.mockClear()
})
afterEach(() => {
document.body.innerHTML = ""
vi.clearAllMocks()
})
// AuthProvider starts with loading=true and only clears it in the initial
// mount probe's finally{}. A user can log in while that probe is still in
// flight (ProtectedRoute renders the login page during loading). If login()
// does not clear loading itself, ProtectedRoute keeps returning null for
// /dashboard and the freshly-authenticated user sees a blank screen until the
// unrelated probe settles.
test("login() clears loading even while the initial profile probe is still pending", async () => {
const { AuthProvider, useAuth } = await import("@/hooks/useAuth")
const captured: { auth?: ReturnType<typeof useAuth> } = {}
function Capture() {
const auth = useAuth()
useEffect(() => {
captured.auth = auth
})
captured.auth = auth
return null
}
await act(async () => {
render(
<AuthProvider>
<Capture />
</AuthProvider>,
)
})
// Initial probe still pending -> loading must be true.
expect(captured.auth!.loading).toBe(true)
await act(async () => {
await captured.auth!.login("u", "p")
})
// Login succeeded; loading must be false so ProtectedRoute renders.
expect(profileStore).toEqual({ id: 42, role: 0 })
expect(captured.auth!.loading).toBe(false)
})
test("loginOauth2() clears loading even while the initial profile probe is still pending", async () => {
const { AuthProvider, useAuth } = await import("@/hooks/useAuth")
const captured: { auth?: ReturnType<typeof useAuth> } = {}
function Capture() {
const auth = useAuth()
useEffect(() => {
captured.auth = auth
})
captured.auth = auth
return null
}
await act(async () => {
render(
<AuthProvider>
<Capture />
</AuthProvider>,
)
})
expect(captured.auth!.loading).toBe(true)
await act(async () => {
await captured.auth!.loginOauth2()
})
expect(captured.auth!.loading).toBe(false)
})
test("logout() clears loading even while the initial profile probe is still pending", async () => {
const { AuthProvider, useAuth } = await import("@/hooks/useAuth")
const captured: { auth?: ReturnType<typeof useAuth> } = {}
function Capture() {
const auth = useAuth()
useEffect(() => {
captured.auth = auth
})
captured.auth = auth
return null
}
await act(async () => {
render(
<AuthProvider>
<Capture />
</AuthProvider>,
)
})
expect(captured.auth!.loading).toBe(true)
await act(async () => {
captured.auth!.logout()
})
expect(captured.auth!.loading).toBe(false)
})
+95
View File
@@ -0,0 +1,95 @@
import { act, render } from "@testing-library/react"
import { useEffect } from "react"
import { afterEach, beforeEach, expect, test, vi } from "vitest"
let profileStore: { id: number; role: number } | undefined
const setProfileSpy = vi.fn((p: any) => {
profileStore = p
})
vi.mock("./useMainStore", () => ({}))
vi.mock("@/hooks/useMainStore", () => ({
useMainStore: (selector: any) => selector({ profile: profileStore, setProfile: setProfileSpy }),
}))
vi.mock("react-i18next", () => ({
useTranslation: () => ({ t: (k: string) => k }),
}))
vi.mock("sonner", () => ({ toast: () => {} }))
const navigate = vi.fn()
vi.mock("react-router-dom", () => ({
useNavigate: () => navigate,
}))
// Deferred initial getProfile() so we can resolve/reject it AFTER login().
let rejectInitial: (e: any) => void
const initialProfilePromise = new Promise((_res, rej) => {
rejectInitial = rej
})
let getProfileCall = 0
const loginRequest = vi.fn(async () => {})
vi.mock("@/api/user", () => ({
getProfile: vi.fn(() => {
getProfileCall++
if (getProfileCall === 1) return initialProfilePromise
return Promise.resolve({ id: 42, role: 0 })
}),
login: () => loginRequest(),
}))
beforeEach(() => {
profileStore = undefined
getProfileCall = 0
setProfileSpy.mockClear()
navigate.mockClear()
})
afterEach(() => {
document.body.innerHTML = ""
vi.clearAllMocks()
})
// The AuthProvider fires getProfile() on mount. While that probe is in flight a
// user can submit the login form (ProtectedRoute renders the login page during
// loading). If the in-flight probe later REJECTS (e.g. it 401'd because the
// user was not yet authenticated), its catch{} must NOT clobber the profile a
// successful login() already set — otherwise the freshly-authenticated user is
// bounced back to the login page.
test("late-rejecting initial profile probe does not clobber a successful login", async () => {
const { AuthProvider, useAuth } = await import("@/hooks/useAuth")
const captured: { auth?: ReturnType<typeof useAuth> } = {}
function Capture() {
const auth = useAuth()
useEffect(() => {
captured.auth = auth
})
return null
}
await act(async () => {
render(
<AuthProvider>
<Capture />
</AuthProvider>,
)
})
// User logs in while the initial probe is still pending.
await act(async () => {
await captured.auth!.login("u", "p")
})
expect(profileStore).toEqual({ id: 42, role: 0 })
// Now the stale initial probe rejects (it was a pre-auth 401).
await act(async () => {
rejectInitial(new Error("401"))
await initialProfilePromise.catch(() => {})
})
// The logged-in profile must survive.
expect(profileStore).toEqual({ id: 42, role: 0 })
expect(setProfileSpy).not.toHaveBeenLastCalledWith(undefined)
})
+87
View File
@@ -0,0 +1,87 @@
import { BatchMoveServerIcon } from "@/components/batch-move-server-icon"
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"
import { afterEach, beforeEach, expect, test, vi } from "vitest"
const toastCalls: Array<{ title: string; description: string }> = []
vi.mock("sonner", () => ({
toast: (title: string, opts?: { description?: string }) => {
toastCalls.push({ title, description: opts?.description ?? "" })
},
}))
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string, vars?: Record<string, unknown>) => {
if (vars && typeof vars.count === "number") {
return `${key}=${vars.count}`
}
return key
},
}),
initReactI18next: { type: "3rdParty", init: () => undefined },
Trans: ({ children }: { children?: React.ReactNode }) => children ?? null,
}))
const batchMoveServer = vi.fn()
vi.mock("@/api/server", () => ({
batchMoveServer: (...args: unknown[]) => batchMoveServer(...args),
}))
beforeEach(() => {
toastCalls.length = 0
batchMoveServer.mockReset()
})
afterEach(() => {
document.body.innerHTML = ""
})
async function openDialogAndSubmit(serverIds: number[], toUser: number) {
render(<BatchMoveServerIcon serverIds={serverIds} />)
await act(async () => {
fireEvent.click(screen.getByRole("button"))
})
const userInput = await screen.findByPlaceholderText("User ID")
await act(async () => {
fireEvent.change(userInput, { target: { value: String(toUser) } })
})
const submit = screen.getByRole("button", { name: "Move" })
await act(async () => {
fireEvent.click(submit)
})
await waitFor(() => expect(toastCalls.length).toBeGreaterThan(0))
}
test("BatchMoveServer toast surfaces agent_too_old count so operator sees the failure", async () => {
batchMoveServer.mockResolvedValueOnce([
{ server_id: 1, status: "agent_too_old", error: "agent build older than v1.18.0" },
{ server_id: 2, status: "pending", transfer_id: 99 },
])
await openDialogAndSubmit([1, 2], 300)
expect(batchMoveServer).toHaveBeenCalledOnce()
const summary = toastCalls[toastCalls.length - 1]
expect(summary, "submission must surface a toast").toBeDefined()
expect(summary.description, "toast must include the agent_too_old count").toContain(
"Transfer.AgentTooOldCount=1",
)
expect(summary.description).toContain("Transfer.PendingCount=1")
})
test("BatchMoveServer toast does not show Done fallback when every server is agent_too_old", async () => {
batchMoveServer.mockResolvedValueOnce([
{ server_id: 1, status: "agent_too_old", error: "older than v1.18.0" },
{ server_id: 2, status: "agent_too_old", error: "older than v1.18.0" },
])
await openDialogAndSubmit([1, 2], 300)
const summary = toastCalls[toastCalls.length - 1]
expect(summary).toBeDefined()
expect(
summary.description,
"all-failed batch must NOT collapse to a generic Done label — operator would think it succeeded",
).toContain("Transfer.AgentTooOldCount=2")
expect(summary.description).not.toBe("Done")
})
+99
View File
@@ -0,0 +1,99 @@
import { afterEach, beforeEach, expect, test, vi } from "vitest"
import { FetcherMethod, fetcher } from "../api/api"
const realFetch = globalThis.fetch
function setCookie(value: string) {
Object.defineProperty(document, "cookie", { value, configurable: true })
}
beforeEach(() => {
setCookie("")
})
afterEach(() => {
globalThis.fetch = realFetch
vi.restoreAllMocks()
})
function jsonOk() {
return new Response(JSON.stringify({ success: true, data: null }), {
status: 200,
headers: { "Content-Type": "application/json" },
})
}
function headerOf(init: RequestInit | undefined, name: string): string | null {
const h = init?.headers
if (!h) return null
if (h instanceof Headers) return h.get(name)
const rec = h as Record<string, string>
const key = Object.keys(rec).find((k) => k.toLowerCase() === name.toLowerCase())
return key ? rec[key] : null
}
// Backend csrfMiddleware (nezha/cmd/dashboard/controller/csrf.go) enforces a
// double-submit cookie on every cookie-auth unsafe method: it rejects the
// request unless X-CSRF-Token header == nz-csrf cookie. The fetcher must mirror
// the cookie into the header for POST/PATCH/PUT/DELETE.
test("POST sends X-CSRF-Token mirrored from nz-csrf cookie", async () => {
setCookie("nz-csrf=abc123; other=1")
const seen: { init?: RequestInit }[] = []
globalThis.fetch = vi.fn(async (_input: any, init?: RequestInit) => {
seen.push({ init })
return jsonOk()
}) as unknown as typeof fetch
await fetcher(FetcherMethod.POST, "/api/v1/api-tokens", { name: "x" })
expect(headerOf(seen[0].init, "X-CSRF-Token")).toBe("abc123")
})
test("DELETE sends X-CSRF-Token mirrored from nz-csrf cookie", async () => {
setCookie("nz-csrf=del-token")
const seen: { init?: RequestInit }[] = []
globalThis.fetch = vi.fn(async (_input: any, init?: RequestInit) => {
seen.push({ init })
return jsonOk()
}) as unknown as typeof fetch
await fetcher(FetcherMethod.DELETE, "/api/v1/api-tokens/7")
expect(headerOf(seen[0].init, "X-CSRF-Token")).toBe("del-token")
})
test("auto refresh-token uses POST (backend route is POST)", async () => {
vi.resetModules()
const { FetcherMethod: M, fetcher: f } = await import("../api/api")
setCookie("nz-jwt=sess; nz-csrf=c")
const seen: { url: string; init?: RequestInit }[] = []
globalThis.fetch = vi.fn(async (input: any, init?: RequestInit) => {
seen.push({ url: String(input), init })
return jsonOk()
}) as unknown as typeof fetch
await f(M.GET, "/api/v1/server")
const refresh = seen.find((s) => s.url.includes("/api/v1/refresh-token"))
expect(refresh, "auto refresh request should be issued").toBeTruthy()
expect(refresh!.init?.method).toBe("POST")
})
// Revoke (DELETE) commonly returns 204 / empty body. The fetcher must not
// blow up on response.json() of an empty body and must resolve successfully.
test("DELETE tolerates an empty 204 response body", async () => {
globalThis.fetch = vi.fn(
async () => new Response(null, { status: 204 }),
) as unknown as typeof fetch
await expect(fetcher(FetcherMethod.DELETE, "/api/v1/api-tokens/9")).resolves.toBeUndefined()
})
test("empty 200 body does not throw", async () => {
globalThis.fetch = vi.fn(
async () => new Response("", { status: 200 }),
) as unknown as typeof fetch
await expect(fetcher(FetcherMethod.DELETE, "/api/v1/api-tokens/9")).resolves.toBeUndefined()
})
+35
View File
@@ -0,0 +1,35 @@
import { afterEach, beforeEach, expect, test, vi } from "vitest"
import { FetcherMethod, fetcher } from "../api/api"
const realFetch = globalThis.fetch
beforeEach(() => {
// Avoid the auto refresh-token branch interfering with assertions.
Object.defineProperty(document, "cookie", { value: "", configurable: true })
})
afterEach(() => {
globalThis.fetch = realFetch
vi.restoreAllMocks()
})
// Regression: fetcher used to collapse GET and DELETE into the same HTTP GET,
// so DELETE callers (e.g. revoke API token) never actually hit the backend
// DELETE route. The wire-level method must match the requested FetcherMethod.
test("fetcher uses HTTP DELETE for FetcherMethod.DELETE", async () => {
const seen: { url: string; init?: RequestInit }[] = []
globalThis.fetch = vi.fn(async (input: any, init?: RequestInit) => {
seen.push({ url: String(input), init })
return new Response(JSON.stringify({ success: true, data: null }), {
status: 200,
headers: { "Content-Type": "application/json" },
})
}) as unknown as typeof fetch
await fetcher(FetcherMethod.DELETE, "/api/v1/api-tokens/42")
expect(seen).toHaveLength(1)
expect(seen[0].init?.method).toBe("DELETE")
expect(seen[0].url).toContain("/api/v1/api-tokens/42")
})
+61
View File
@@ -0,0 +1,61 @@
import { afterEach, beforeEach, expect, test, vi } from "vitest"
const realFetch = globalThis.fetch
function setCookie(value: string) {
Object.defineProperty(document, "cookie", { value, configurable: true })
}
function jsonOk() {
return new Response(JSON.stringify({ success: true, data: null }), {
status: 200,
headers: { "Content-Type": "application/json" },
})
}
beforeEach(() => {
setCookie("")
vi.resetModules()
})
afterEach(() => {
globalThis.fetch = realFetch
vi.restoreAllMocks()
})
// Auto-refresh is a POST behind the CSRF gate. When the session still lacks the
// nz-csrf cookie (e.g. just upgraded), firing the refresh without a header only
// burns the 1h throttle window and 403s. The refresh must instead wait until a
// CSRF token is available so it can actually succeed once the cookie is seeded.
test("auto refresh is deferred while nz-csrf cookie is missing", async () => {
const { FetcherMethod, fetcher } = await import("../api/api")
setCookie("nz-jwt=session") // jwt present, but no nz-csrf yet
const urls: string[] = []
globalThis.fetch = vi.fn(async (input: any) => {
urls.push(String(input))
return jsonOk()
}) as unknown as typeof fetch
await fetcher(FetcherMethod.GET, "/api/v1/server")
expect(urls.some((u) => u.includes("/api/v1/refresh-token"))).toBe(false)
})
// Once the cookie exists, the next GET must be allowed to fire the refresh —
// proving the missing-cookie skip did not permanently consume the throttle.
test("auto refresh fires after nz-csrf cookie becomes available", async () => {
const { FetcherMethod, fetcher } = await import("../api/api")
const urls: string[] = []
globalThis.fetch = vi.fn(async (input: any) => {
urls.push(String(input))
return jsonOk()
}) as unknown as typeof fetch
setCookie("nz-jwt=session") // first GET: no csrf, refresh skipped
await fetcher(FetcherMethod.GET, "/api/v1/server")
expect(urls.some((u) => u.includes("/api/v1/refresh-token"))).toBe(false)
setCookie("nz-jwt=session; nz-csrf=seeded") // backend seeded it
await fetcher(FetcherMethod.GET, "/api/v1/server")
expect(urls.some((u) => u.includes("/api/v1/refresh-token"))).toBe(true)
})
+249
View File
@@ -0,0 +1,249 @@
import { act, render, waitFor } from "@testing-library/react"
import { afterEach, beforeEach, expect, test, vi } from "vitest"
import { FMComponent } from "../components/fm"
type DivProps = React.ComponentPropsWithoutRef<"div"> & { asChild?: boolean }
type ButtonProps = React.ComponentPropsWithoutRef<"button"> & {
asChild?: boolean
size?: string
variant?: string
}
type IconButtonProps = ButtonProps & { icon?: string }
type SentWebSocketData = string | ArrayBufferLike | Blob | ArrayBufferView
vi.mock("../components/ui/button", () => ({
Button: (props: ButtonProps) => {
const { asChild, size, variant, ...buttonProps } = props
void asChild
void size
void variant
return <button {...buttonProps} />
},
}))
vi.mock("../components/ui/input", () => ({
Input: (props: React.ComponentPropsWithoutRef<"input">) => <input {...props} />,
}))
vi.mock("../components/ui/table", () => ({
Table: (props: React.ComponentPropsWithoutRef<"table">) => <table {...props} />,
TableHeader: (props: React.ComponentPropsWithoutRef<"thead">) => <thead {...props} />,
TableBody: (props: React.ComponentPropsWithoutRef<"tbody">) => <tbody {...props} />,
TableRow: (props: React.ComponentPropsWithoutRef<"tr">) => <tr {...props} />,
TableCell: (props: React.ComponentPropsWithoutRef<"td">) => <td {...props} />,
TableHead: (props: React.ComponentPropsWithoutRef<"th">) => <th {...props} />,
}))
vi.mock("../components/ui/dropdown-menu", () => ({
DropdownMenu: (props: DivProps) => <div {...props} />,
DropdownMenuTrigger: (props: DivProps) => <div {...props} />,
DropdownMenuContent: (props: DivProps) => <div {...props} />,
DropdownMenuItem: (props: DivProps) => <div {...props} />,
}))
vi.mock("../components/ui/alert-dialog", () => ({
AlertDialog: (props: DivProps) => <div {...props} />,
AlertDialogTrigger: (props: DivProps) => <div {...props} />,
AlertDialogContent: (props: DivProps) => <div {...props} />,
AlertDialogHeader: (props: DivProps) => <div {...props} />,
AlertDialogFooter: (props: DivProps) => <div {...props} />,
AlertDialogTitle: (props: DivProps) => <div {...props} />,
AlertDialogDescription: (props: DivProps) => <div {...props} />,
AlertDialogCancel: (props: DivProps) => <div {...props} />,
AlertDialogAction: (props: DivProps) => <div {...props} />,
}))
vi.mock("../components/ui/drawer", () => ({
Drawer: (props: DivProps) => <div {...props} />,
DrawerContent: (props: DivProps) => <div {...props} />,
DrawerHeader: (props: DivProps) => <div {...props} />,
DrawerTitle: (props: DivProps) => <div {...props} />,
DrawerTrigger: (props: DivProps) => <div {...props} />,
}))
vi.mock("../components/xui/overlayless-sheet", () => ({
Sheet: (props: DivProps) => <div {...props} />,
SheetContent: (props: DivProps) => <div {...props} />,
SheetDescription: (props: DivProps) => <div {...props} />,
SheetHeader: (props: DivProps) => <div {...props} />,
SheetTitle: (props: DivProps) => <div {...props} />,
SheetTrigger: (props: DivProps) => <div {...props} />,
}))
vi.mock("../components/xui/filepath", () => ({
Filepath: () => <div data-testid="filepath" />,
}))
vi.mock("../components/xui/icon-button", () => ({
IconButton: (props: IconButtonProps) => {
const { asChild, icon, size, variant, ...buttonProps } = props
void asChild
void icon
void size
void variant
return <button {...buttonProps} />
},
}))
vi.mock("../components/xui/virtulized-data-table", () => ({
DataTable: () => <div data-testid="data-table" />,
}))
vi.mock("lucide-react", () => ({
File: () => <div data-testid="file-icon" />,
Folder: () => <div data-testid="folder-icon" />,
}))
const translate = (key: string) => key
vi.mock("react-i18next", () => ({
useTranslation: () => ({ t: translate }),
initReactI18next: { type: "3rdParty", init: () => {} },
}))
vi.mock("sonner", () => ({ toast: vi.fn() }))
vi.mock("@/lib/utils", () => ({
copyToClipboard: vi.fn(),
fm: {
parseFMList: async (buf: ArrayBufferLike) => {
const view = new DataView(buf)
const pathLength = view.getUint32(4, false)
const pathBytes = new Uint8Array(buf, 8, pathLength)
return { path: new TextDecoder().decode(pathBytes), fmList: [] }
},
},
fmWorker: new Worker(""),
formatPath: (path: string) => path,
}))
const webSockets: MockWebSocket[] = []
class MockWebSocket extends EventTarget implements WebSocket {
static readonly CONNECTING = 0
static readonly OPEN = 1
static readonly CLOSING = 2
static readonly CLOSED = 3
readonly CONNECTING = MockWebSocket.CONNECTING
readonly OPEN = MockWebSocket.OPEN
readonly CLOSING = MockWebSocket.CLOSING
readonly CLOSED = MockWebSocket.CLOSED
readonly bufferedAmount = 0
readonly extensions = ""
readonly protocol = ""
readonly url: string
binaryType: BinaryType = "arraybuffer"
onclose: ((this: WebSocket, ev: CloseEvent) => unknown) | null = null
onerror: ((this: WebSocket, ev: Event) => unknown) | null = null
onmessage: ((this: WebSocket, ev: MessageEvent) => unknown) | null = null
onopen: ((this: WebSocket, ev: Event) => unknown) | null = null
readyState: 0 | 1 | 2 | 3 = MockWebSocket.CONNECTING
sent: SentWebSocketData[] = []
constructor(url: string | URL) {
super()
this.url = url.toString()
webSockets.push(this)
}
addEventListener<K extends keyof WebSocketEventMap>(
type: K,
listener: (this: WebSocket, ev: WebSocketEventMap[K]) => unknown,
options?: boolean | AddEventListenerOptions,
): void
addEventListener(
type: string,
listener: EventListenerOrEventListenerObject | null,
options?: boolean | AddEventListenerOptions,
): void {
super.addEventListener(type, listener, options)
}
removeEventListener<K extends keyof WebSocketEventMap>(
type: K,
listener: (this: WebSocket, ev: WebSocketEventMap[K]) => unknown,
options?: boolean | EventListenerOptions,
): void
removeEventListener(
type: string,
listener: EventListenerOrEventListenerObject | null,
options?: boolean | EventListenerOptions,
): void {
super.removeEventListener(type, listener, options)
}
close(): void {
this.readyState = MockWebSocket.CLOSED
}
open(): void {
this.readyState = MockWebSocket.OPEN
this.onopen?.call(this, new Event("open"))
}
send(data: SentWebSocketData): void {
this.sent.push(data)
}
}
beforeEach(() => {
webSockets.length = 0
globalThis.WebSocket = MockWebSocket
})
afterEach(() => {
vi.clearAllMocks()
})
const encodeFileNameMessage = (path: string) => {
const identifier = new Uint8Array([0x4e, 0x5a, 0x46, 0x4e])
const pathBytes = new TextEncoder().encode(path)
const payload = new Uint8Array(identifier.length + 4 + pathBytes.length)
payload.set(identifier, 0)
new DataView(payload.buffer).setUint32(identifier.length, pathBytes.length, false)
payload.set(pathBytes, identifier.length + 4)
return payload.buffer
}
const encodeCompleteMessage = () => new Uint8Array([0x4e, 0x5a, 0x55, 0x50]).buffer
const decodeListPath = (data: SentWebSocketData) => {
if (!ArrayBuffer.isView(data)) return null
const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
if (bytes[0] !== 0) return null
return new TextDecoder().decode(bytes.slice(1))
}
test("FM websocket lifecycle reuses the socket on path changes", async () => {
const { unmount } = render(<FMComponent wsUrl="/ws/file/test" />)
await waitFor(() => {
expect(webSockets).toHaveLength(1)
})
const socket = webSockets[0]
act(() => {
socket.open()
})
await act(async () => {
await socket.onmessage?.call(
socket,
new MessageEvent("message", { data: encodeFileNameMessage("/new/path") }),
)
})
await waitFor(() => {
expect(webSockets).toHaveLength(1)
expect(decodeListPath(socket.sent[socket.sent.length - 1])).toBe("/new/path")
})
socket.sent.length = 0
await act(async () => {
await socket.onmessage?.call(
socket,
new MessageEvent("message", { data: encodeCompleteMessage() }),
)
})
expect(webSockets).toHaveLength(1)
expect(decodeListPath(socket.sent[socket.sent.length - 1])).toBe("/new/path")
unmount()
expect(socket.readyState).toBe(MockWebSocket.CLOSED)
})
+95
View File
@@ -0,0 +1,95 @@
import { act, render } from "@testing-library/react"
import { MemoryRouter, Route, Routes } from "react-router-dom"
import { afterEach, beforeEach, expect, test, vi } from "vitest"
// ProtectedRoute must NOT mount its children while AuthProvider is still
// running the initial getProfile() probe — except for the /dashboard/login
// path itself. Mounting the protected subtree during the probe would fire
// authenticated SWR fetches like /api/v1/setting before auth is confirmed.
// Without this contract, a regression in protect.tsx silently re-introduces
// pre-auth traffic and a flash of protected UI before redirect.
let mockProfile: { id: number; role: number } | undefined
let mockLoading = false
vi.mock("@/hooks/useAuth", () => ({
useAuth: () => ({ profile: mockProfile, loading: mockLoading }),
}))
beforeEach(() => {
mockProfile = undefined
mockLoading = true
})
afterEach(() => {
document.body.innerHTML = ""
})
function renderAtPath(path: string, child: React.ReactNode) {
return import("@/routes/protect").then(({ default: ProtectedRoute }) => {
return act(async () => {
render(
<MemoryRouter initialEntries={[path]}>
<Routes>
<Route
path="/dashboard/login"
element={<ProtectedRoute>{child}</ProtectedRoute>}
/>
<Route
path="/dashboard/*"
element={<ProtectedRoute>{child}</ProtectedRoute>}
/>
</Routes>
</MemoryRouter>,
)
})
})
}
test("ProtectedRoute does not mount children for protected paths while auth is loading", async () => {
await renderAtPath("/dashboard", <div data-testid="protected-child">protected</div>)
expect(document.querySelector("[data-testid='protected-child']")).toBeNull()
})
test("ProtectedRoute renders children on the login page even while auth is loading", async () => {
await renderAtPath("/dashboard/login", <div data-testid="login-child">login</div>)
expect(document.querySelector("[data-testid='login-child']")).not.toBeNull()
})
test("ProtectedRoute redirects unauthenticated users without mounting protected children", async () => {
mockLoading = false
mockProfile = undefined
const { default: ProtectedRoute } = await import("@/routes/protect")
await act(async () => {
render(
<MemoryRouter initialEntries={["/dashboard"]}>
<Routes>
<Route
path="/dashboard/login"
element={
<ProtectedRoute>
<div data-testid="login-child">login</div>
</ProtectedRoute>
}
/>
<Route
path="/dashboard/*"
element={
<ProtectedRoute>
<div data-testid="protected-child">protected</div>
</ProtectedRoute>
}
/>
</Routes>
</MemoryRouter>,
)
})
expect(document.querySelector("[data-testid='protected-child']")).toBeNull()
expect(document.querySelector("[data-testid='login-child']")).not.toBeNull()
})
test("ProtectedRoute renders children once an authenticated profile resolves", async () => {
mockLoading = false
mockProfile = { id: 1, role: 0 }
await renderAtPath("/dashboard", <div data-testid="protected-child">protected</div>)
expect(document.querySelector("[data-testid='protected-child']")).not.toBeNull()
})
+74
View File
@@ -0,0 +1,74 @@
import { act, render } from "@testing-library/react"
import { afterEach, beforeEach, expect, test, vi } from "vitest"
vi.mock("sonner", () => ({ toast: () => {} }))
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (k: string) => k,
i18n: { language: "en", changeLanguage: () => {} },
}),
initReactI18next: { type: "3rdParty", init: () => undefined },
Trans: ({ children }: { children?: React.ReactNode }) => children ?? null,
}))
let mockProfile: { id: number; role: number } | undefined
let mockLoading = false
vi.mock("@/hooks/useAuth", () => ({
useAuth: () => ({ profile: mockProfile, loading: mockLoading }),
}))
vi.mock("@/hooks/useSetting", () => ({
default: () => ({ data: undefined, mutate: () => {} }),
}))
vi.mock("@/hooks/useNotfication", () => ({
useNotification: () => ({ notifierGroup: [] }),
}))
vi.mock("@/api/settings", () => ({ updateSettings: vi.fn() }))
const navigateRenders: string[] = []
vi.mock("react-router-dom", async () => {
const actual = await vi.importActual<any>("react-router-dom")
return {
...actual,
Navigate: ({ to }: { to: string }) => {
navigateRenders.push(to)
return <div data-testid="nav-stub">redirect:{to}</div>
},
}
})
beforeEach(() => {
navigateRenders.length = 0
mockProfile = undefined
mockLoading = true
})
afterEach(() => {
document.body.innerHTML = ""
})
// SettingsPage 在 profile 还没 fetch 完成时不能就把用户当作非管理员重定向。
// useAuth.loading=true 表示请求未回来;此时必须按"加载中"渲染,不能 Navigate
// 到 /dashboard/settings/api-tokens,否则管理员每次直达 /dashboard/settings
// 都会先闪一下到 api-tokens 页。
test("SettingsPage waits for auth load before redirecting non-admin", async () => {
const { default: SettingsPage } = await import("@/routes/settings")
await act(async () => {
render(<SettingsPage />)
})
expect(navigateRenders).toEqual([])
})
// 一旦 loading=false 且确认 profile 不是 admin,才允许跳转。
test("SettingsPage redirects to api-tokens once auth resolves and user is not admin", async () => {
mockLoading = false
mockProfile = { id: 1, role: 1 }
const { default: SettingsPage } = await import("@/routes/settings")
await act(async () => {
render(<SettingsPage />)
})
expect(navigateRenders).toContain("/dashboard/settings/api-tokens")
})
+109
View File
@@ -0,0 +1,109 @@
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"
import { MemoryRouter } from "react-router-dom"
import { afterEach, beforeEach, expect, test, vi } from "vitest"
const toastCalls: string[] = []
vi.mock("sonner", () => ({
toast: (msg: string) => {
toastCalls.push(msg)
},
}))
const changeLanguageCalls: string[] = []
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (k: string) => k,
i18n: {
language: "en",
changeLanguage: (lng: string) => {
changeLanguageCalls.push(lng)
},
},
}),
initReactI18next: { type: "3rdParty", init: () => undefined },
Trans: ({ children }: { children?: React.ReactNode }) => children ?? null,
}))
vi.mock("@/hooks/useAuth", () => ({
useAuth: () => ({ profile: { id: 1, role: 0 }, loading: false }),
}))
const validConfig = {
config: {
site_name: "Nezha",
language: "zh-CN",
user_template: "user-dist",
cover: 1,
ip_change_notification_group_id: 0,
},
frontend_templates: [],
}
vi.mock("@/hooks/useSetting", () => ({
default: () => ({ data: validConfig, mutate: vi.fn() }),
}))
vi.mock("@/hooks/useNotfication", () => ({
useNotification: () => ({ notifierGroup: [] }),
}))
const updateSettings = vi.fn()
vi.mock("@/api/settings", () => ({
updateSettings: (...args: unknown[]) => updateSettings(...args),
}))
beforeEach(() => {
toastCalls.length = 0
changeLanguageCalls.length = 0
updateSettings.mockReset()
})
afterEach(() => {
document.body.innerHTML = ""
})
test("SettingsPage does not toast Success when updateSettings rejects", async () => {
updateSettings.mockRejectedValue(new Error("boom"))
const { default: SettingsPage } = await import("@/routes/settings")
await act(async () => {
render(
<MemoryRouter>
<SettingsPage />
</MemoryRouter>,
)
})
const submit = screen.getByRole("button", { name: /Confirm|Submit|Save/i })
await act(async () => {
fireEvent.click(submit)
})
await waitFor(() => expect(updateSettings).toHaveBeenCalled())
expect(toastCalls).toContain("Error")
expect(toastCalls).not.toContain("Success")
expect(changeLanguageCalls).toEqual([])
})
test("SettingsPage toasts Success when updateSettings resolves", async () => {
updateSettings.mockResolvedValue(undefined)
const { default: SettingsPage } = await import("@/routes/settings")
await act(async () => {
render(
<MemoryRouter>
<SettingsPage />
</MemoryRouter>,
)
})
const submit = screen.getByRole("button", { name: /Confirm|Submit|Save/i })
await act(async () => {
fireEvent.click(submit)
})
await waitFor(() => expect(updateSettings).toHaveBeenCalled())
expect(toastCalls).toContain("Success")
expect(toastCalls).not.toContain("Error")
})
+37
View File
@@ -0,0 +1,37 @@
class TestWorker implements Worker {
onmessage: ((this: Worker, ev: MessageEvent) => unknown) | null = null
onmessageerror: ((this: Worker, ev: MessageEvent) => unknown) | null = null
onerror: ((this: AbstractWorker, ev: ErrorEvent) => unknown) | null = null
addEventListener(): void {}
removeEventListener(): void {}
dispatchEvent(): boolean {
return true
}
postMessage(): void {}
terminate(): void {}
}
class TestResizeObserver implements ResizeObserver {
observe(): void {}
unobserve(): void {}
disconnect(): void {}
}
class TestIntersectionObserver implements IntersectionObserver {
readonly root: Element | Document | null = null
readonly rootMargin = ""
readonly scrollMargin = ""
readonly thresholds: ReadonlyArray<number> = []
observe(): void {}
unobserve(): void {}
disconnect(): void {}
takeRecords(): IntersectionObserverEntry[] {
return []
}
}
globalThis.Worker = TestWorker
globalThis.ResizeObserver = TestResizeObserver
globalThis.IntersectionObserver = TestIntersectionObserver
+135
View File
@@ -0,0 +1,135 @@
import type { ModelServerTransfer } from "@/types"
import { act, render, screen, waitFor } from "@testing-library/react"
import { afterEach, beforeEach, expect, test, vi } from "vitest"
const toastCalls: Array<{ title: string; description: string }> = []
vi.mock("sonner", () => ({
toast: (title: string, opts?: { description?: string }) => {
toastCalls.push({ title, description: opts?.description ?? "" })
},
}))
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
initReactI18next: { type: "3rdParty", init: () => undefined },
Trans: ({ children }: { children?: React.ReactNode }) => children ?? null,
}))
const cancelServerTransfer = vi.fn()
const retryServerTransfer = vi.fn()
vi.mock("@/api/transfer", () => ({
cancelServerTransfer: (...args: unknown[]) => cancelServerTransfer(...args),
retryServerTransfer: (...args: unknown[]) => retryServerTransfer(...args),
}))
const swrFetcher = vi.fn()
vi.mock("@/api/api", () => ({
swrFetcher: (...args: unknown[]) => swrFetcher(...args),
}))
vi.mock("swr", () => ({
default: (_key: string, _fetcher: unknown) => ({
data: mockedRows,
mutate: vi.fn(),
error: undefined,
}),
}))
let mockProfile: { id: number; role: number } | undefined
vi.mock("@/hooks/useAuth", () => ({
useAuth: () => ({ profile: mockProfile }),
}))
vi.mock("@/hooks/useMainStore", () => ({
useMainStore: (selector?: (s: { profile?: { id: number; role: number } }) => unknown) => {
const store = { profile: mockProfile }
return selector ? selector(store) : store
},
}))
let mockedRows: ModelServerTransfer[] = []
beforeEach(() => {
toastCalls.length = 0
cancelServerTransfer.mockReset()
retryServerTransfer.mockReset()
mockedRows = []
mockProfile = undefined
})
afterEach(() => {
document.body.innerHTML = ""
})
function makeRow(overrides: Partial<ModelServerTransfer>): ModelServerTransfer {
return {
id: 1,
server_id: 10,
from_user_id: 100,
to_user_id: 200,
initiator_id: 100,
status: 0,
last_error: "",
acked_at: "",
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
...overrides,
} as ModelServerTransfer
}
async function renderPage() {
const { default: TransferPage } = await import("@/routes/transfer")
await act(async () => {
render(<TransferPage />)
})
await waitFor(() => {
expect(screen.getByText("Transfer.Title")).toBeTruthy()
})
}
test("non-admin member who is the FromUserID sees Cancel for pending rows but NOT Retry for terminal rows", async () => {
mockProfile = { id: 100, role: 1 }
mockedRows = [
makeRow({ id: 1, status: 0, from_user_id: 100 }),
makeRow({ id: 2, status: 2, from_user_id: 100 }),
makeRow({ id: 3, status: 4, from_user_id: 100 }),
]
await renderPage()
expect(screen.queryAllByRole("button", { name: "Cancel" }).length).toBeGreaterThan(0)
expect(
screen.queryAllByRole("button", { name: "Transfer.Retry" }),
"Retry is admin-only on the backend; rendering it for members produces guaranteed permission_denied on click",
).toHaveLength(0)
})
test("non-admin member who is only the ToUserID or InitiatorID sees neither Cancel nor Retry", async () => {
mockProfile = { id: 200, role: 1 }
mockedRows = [
makeRow({ id: 1, status: 0, from_user_id: 100, to_user_id: 200 }),
makeRow({ id: 2, status: 3, from_user_id: 100, to_user_id: 200 }),
]
await renderPage()
expect(
screen.queryAllByRole("button", { name: "Cancel" }),
"backend cancelServerTransfer rejects non-admins that are not the FromUserID; UI must not pretend it works",
).toHaveLength(0)
expect(
screen.queryAllByRole("button", { name: "Transfer.Retry" }),
"backend retryServerTransfer is admin-only; non-admin must not see the button",
).toHaveLength(0)
})
test("admin sees both Cancel for pending and Retry for terminal", async () => {
mockProfile = { id: 1, role: 0 }
mockedRows = [makeRow({ id: 1, status: 0 }), makeRow({ id: 2, status: 2 })]
await renderPage()
expect(screen.queryAllByRole("button", { name: "Cancel" }).length).toBe(1)
expect(screen.queryAllByRole("button", { name: "Transfer.Retry" }).length).toBe(1)
})

Some files were not shown because too many files have changed in this diff Show More