fix(api): align alerts/commands contracts and ship full-site test suite.

Restore alert history filters and stats fields, paginate command/session logs for the SPA, show script labels on schedules, and land integration/E2E coverage with rebuilt web assets.
This commit is contained in:
Nexus Agent
2026-06-09 02:13:13 +08:00
parent 091fb97291
commit 0f6f91e61a
7017 changed files with 5553 additions and 79482 deletions
+42 -2
View File
@@ -35,18 +35,58 @@ jobs:
run: mypy server/ --ignore-missing-imports
continue-on-error: true # Type errors don't block deployment yet
- name: Run tests with coverage
- name: Run fast tests with coverage
run: |
coverage run -m pytest tests/ -v --tb=short
coverage run -m pytest tests/ -m "not chain and not slow" -q --tb=short
coverage report --fail-under=50
coverage xml
- name: Run chain tests
run: pytest tests/chain -m chain -q --tb=short
continue-on-error: true
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage.xml
# ── CI: E2E (main only, optional secrets) ──
e2e:
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install frontend dependencies
working-directory: frontend
run: npm ci
- name: Install Playwright Chromium
working-directory: frontend
run: npx playwright install chromium
- name: Run Playwright E2E
working-directory: frontend
env:
NEXUS_E2E_BASE: ${{ secrets.NEXUS_E2E_BASE || 'http://127.0.0.1:8600' }}
NEXUS_E2E_USER: ${{ secrets.NEXUS_E2E_USER || 'admin' }}
NEXUS_E2E_PASSWORD: ${{ secrets.NEXUS_E2E_PASSWORD }}
run: |
if [ -z "$NEXUS_E2E_PASSWORD" ]; then
echo "NEXUS_E2E_PASSWORD not set — skip E2E"
exit 0
fi
npm run test:e2e
continue-on-error: true
# ── CD: Deploy ──
deploy:
needs: test # Only deploy if tests pass
+1
View File
@@ -26,6 +26,7 @@ ENV/
.megamemory/
.playwright-mcp/
frontend/test-results/
frontend/e2e/.auth/
tmp/
# IDE
+200
View File
@@ -926,3 +926,203 @@
{"ts":"2026-06-08T13:22:42+08:00","date":"2026-06-08","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-08T13:22:42+08:00","date":"2026-06-08","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-08T13:22:42+08:00","date":"2026-06-08","gate":"summary","result":"BLOCK","detail":"6/7"}
{"ts":"2026-06-08T13:23:04+08:00","date":"2026-06-08","gate":"changelog","result":"PASS","detail":"2026-06-08-notify-recovery-respects-resource-toggle.md 44lines"}
{"ts":"2026-06-08T13:23:04+08:00","date":"2026-06-08","gate":"audit","result":"PASS","detail":"2026-06-08-notify-recovery-resource-toggle-deploy.md"}
{"ts":"2026-06-08T13:23:04+08:00","date":"2026-06-08","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-08T13:23:04+08:00","date":"2026-06-08","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-08T13:23:04+08:00","date":"2026-06-08","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-08T13:23:04+08:00","date":"2026-06-08","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-08T13:23:04+08:00","date":"2026-06-08","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-08T13:23:04+08:00","date":"2026-06-08","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-08T13:23:04+08:00","date":"2026-06-08","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-08T13:23:04+08:00","date":"2026-06-08","gate":"summary","result":"PASS","detail":"7/7"}
{"ts":"2026-06-08T13:45:16+08:00","date":"2026-06-08","gate":"changelog","result":"PASS","detail":"2026-06-08-server-inline-detail-contrast.md 22lines"}
{"ts":"2026-06-08T13:45:16+08:00","date":"2026-06-08","gate":"audit","result":"PASS","detail":"2026-06-08-server-inline-detail-contrast-deploy.md"}
{"ts":"2026-06-08T13:45:16+08:00","date":"2026-06-08","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-08T13:45:16+08:00","date":"2026-06-08","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-08T13:45:16+08:00","date":"2026-06-08","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-08T13:45:16+08:00","date":"2026-06-08","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-08T13:45:16+08:00","date":"2026-06-08","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-08T13:45:16+08:00","date":"2026-06-08","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-08T13:45:16+08:00","date":"2026-06-08","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-08T13:45:16+08:00","date":"2026-06-08","gate":"summary","result":"PASS","detail":"7/7"}
{"ts":"2026-06-08T14:09:53+08:00","date":"2026-06-08","gate":"changelog","result":"PASS","detail":"2026-06-08-unset-path-detect-selection-count.md 17lines"}
{"ts":"2026-06-08T14:09:53+08:00","date":"2026-06-08","gate":"audit","result":"PASS","detail":"2026-06-08-unset-path-detect-selection-deploy.md"}
{"ts":"2026-06-08T14:09:53+08:00","date":"2026-06-08","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-08T14:09:53+08:00","date":"2026-06-08","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-08T14:09:53+08:00","date":"2026-06-08","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-08T14:09:53+08:00","date":"2026-06-08","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-08T14:09:53+08:00","date":"2026-06-08","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-08T14:09:53+08:00","date":"2026-06-08","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-08T14:09:53+08:00","date":"2026-06-08","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-08T14:09:53+08:00","date":"2026-06-08","gate":"summary","result":"PASS","detail":"7/7"}
{"ts":"2026-06-08T14:36:00+08:00","date":"2026-06-08","gate":"changelog","result":"PASS","detail":"2026-06-08-unset-path-table-sort.md 25lines"}
{"ts":"2026-06-08T14:36:00+08:00","date":"2026-06-08","gate":"audit","result":"PASS","detail":"2026-06-08-unset-path-table-sort-deploy.md"}
{"ts":"2026-06-08T14:36:00+08:00","date":"2026-06-08","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-08T14:36:00+08:00","date":"2026-06-08","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-08T14:36:00+08:00","date":"2026-06-08","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-08T14:36:00+08:00","date":"2026-06-08","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-08T14:36:00+08:00","date":"2026-06-08","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-08T14:36:00+08:00","date":"2026-06-08","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-08T14:36:00+08:00","date":"2026-06-08","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-08T14:36:00+08:00","date":"2026-06-08","gate":"summary","result":"PASS","detail":"7/7"}
{"ts":"2026-06-08T14:50:10+08:00","date":"2026-06-08","gate":"changelog","result":"PASS","detail":"2026-06-08-settings-pool-size-string-payload.md 23lines"}
{"ts":"2026-06-08T14:50:10+08:00","date":"2026-06-08","gate":"audit","result":"PASS","detail":"2026-06-08-settings-pool-size-deploy.md"}
{"ts":"2026-06-08T14:50:10+08:00","date":"2026-06-08","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-08T14:50:10+08:00","date":"2026-06-08","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-08T14:50:10+08:00","date":"2026-06-08","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-08T14:50:10+08:00","date":"2026-06-08","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-08T14:50:10+08:00","date":"2026-06-08","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-08T14:50:10+08:00","date":"2026-06-08","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-08T14:50:10+08:00","date":"2026-06-08","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-08T14:50:10+08:00","date":"2026-06-08","gate":"summary","result":"PASS","detail":"7/7"}
{"ts":"2026-06-08T14:52:38+08:00","date":"2026-06-08","gate":"changelog","result":"PASS","detail":"2026-06-08-settings-form-validate-zh-errors.md 32lines"}
{"ts":"2026-06-08T14:52:38+08:00","date":"2026-06-08","gate":"audit","result":"PASS","detail":"2026-06-08-settings-form-validate-zh-deploy.md"}
{"ts":"2026-06-08T14:52:38+08:00","date":"2026-06-08","gate":"test","result":"BLOCK","detail":"tests failed"}
{"ts":"2026-06-08T14:52:38+08:00","date":"2026-06-08","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-08T14:52:38+08:00","date":"2026-06-08","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-08T14:52:38+08:00","date":"2026-06-08","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-08T14:52:38+08:00","date":"2026-06-08","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-08T14:52:38+08:00","date":"2026-06-08","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-08T14:52:38+08:00","date":"2026-06-08","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-08T14:52:38+08:00","date":"2026-06-08","gate":"summary","result":"BLOCK","detail":"6/7"}
{"ts":"2026-06-08T14:52:50+08:00","date":"2026-06-08","gate":"changelog","result":"PASS","detail":"2026-06-08-settings-form-validate-zh-errors.md 32lines"}
{"ts":"2026-06-08T14:52:50+08:00","date":"2026-06-08","gate":"audit","result":"PASS","detail":"2026-06-08-settings-form-validate-zh-deploy.md"}
{"ts":"2026-06-08T14:52:50+08:00","date":"2026-06-08","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-08T14:52:50+08:00","date":"2026-06-08","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-08T14:52:50+08:00","date":"2026-06-08","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-08T14:52:50+08:00","date":"2026-06-08","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-08T14:52:50+08:00","date":"2026-06-08","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-08T14:52:50+08:00","date":"2026-06-08","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-08T14:52:50+08:00","date":"2026-06-08","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-08T14:52:50+08:00","date":"2026-06-08","gate":"summary","result":"PASS","detail":"7/7"}
{"ts":"2026-06-08T15:06:35+08:00","date":"2026-06-08","gate":"changelog","result":"PASS","detail":"2026-06-08-server-ids-batch-max-2000.md 27lines"}
{"ts":"2026-06-08T15:06:35+08:00","date":"2026-06-08","gate":"audit","result":"BLOCK","detail":"missing sections"}
{"ts":"2026-06-08T15:06:35+08:00","date":"2026-06-08","gate":"test","result":"BLOCK","detail":"tests failed"}
{"ts":"2026-06-08T15:06:35+08:00","date":"2026-06-08","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-08T15:06:35+08:00","date":"2026-06-08","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-08T15:06:35+08:00","date":"2026-06-08","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-08T15:06:35+08:00","date":"2026-06-08","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
{"ts":"2026-06-08T15:06:35+08:00","date":"2026-06-08","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-08T15:06:35+08:00","date":"2026-06-08","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-08T15:06:35+08:00","date":"2026-06-08","gate":"summary","result":"BLOCK","detail":"4/7"}
{"ts":"2026-06-08T15:06:42+08:00","date":"2026-06-08","gate":"changelog","result":"PASS","detail":"2026-06-08-server-ids-batch-max-2000.md 27lines"}
{"ts":"2026-06-08T15:06:42+08:00","date":"2026-06-08","gate":"audit","result":"BLOCK","detail":"missing sections"}
{"ts":"2026-06-08T15:06:42+08:00","date":"2026-06-08","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-08T15:06:42+08:00","date":"2026-06-08","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-08T15:06:42+08:00","date":"2026-06-08","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-08T15:06:42+08:00","date":"2026-06-08","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-08T15:06:42+08:00","date":"2026-06-08","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
{"ts":"2026-06-08T15:06:42+08:00","date":"2026-06-08","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-08T15:06:42+08:00","date":"2026-06-08","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-08T15:06:42+08:00","date":"2026-06-08","gate":"summary","result":"BLOCK","detail":"5/7"}
{"ts":"2026-06-08T15:06:49+08:00","date":"2026-06-08","gate":"changelog","result":"PASS","detail":"2026-06-08-server-ids-batch-max-2000.md 27lines"}
{"ts":"2026-06-08T15:06:49+08:00","date":"2026-06-08","gate":"audit","result":"BLOCK","detail":"missing sections"}
{"ts":"2026-06-08T15:06:49+08:00","date":"2026-06-08","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-08T15:06:49+08:00","date":"2026-06-08","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-08T15:06:49+08:00","date":"2026-06-08","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-08T15:06:49+08:00","date":"2026-06-08","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-08T15:06:49+08:00","date":"2026-06-08","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
{"ts":"2026-06-08T15:06:49+08:00","date":"2026-06-08","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-08T15:06:49+08:00","date":"2026-06-08","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-08T15:06:49+08:00","date":"2026-06-08","gate":"summary","result":"BLOCK","detail":"5/7"}
{"ts":"2026-06-08T15:08:03+08:00","date":"2026-06-08","gate":"changelog","result":"PASS","detail":"2026-06-08-server-ids-batch-max-2000.md 27lines"}
{"ts":"2026-06-08T15:08:03+08:00","date":"2026-06-08","gate":"audit","result":"PASS","detail":"2026-06-08-server-ids-batch-max-deploy.md"}
{"ts":"2026-06-08T15:08:03+08:00","date":"2026-06-08","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-08T15:08:03+08:00","date":"2026-06-08","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-08T15:08:03+08:00","date":"2026-06-08","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-08T15:08:03+08:00","date":"2026-06-08","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-08T15:08:03+08:00","date":"2026-06-08","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-08T15:08:03+08:00","date":"2026-06-08","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-08T15:08:03+08:00","date":"2026-06-08","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-08T15:08:03+08:00","date":"2026-06-08","gate":"summary","result":"PASS","detail":"7/7"}
{"ts":"2026-06-08T15:23:16+08:00","date":"2026-06-08","gate":"changelog","result":"PASS","detail":"2026-06-08-full-align-deploy.md 33lines"}
{"ts":"2026-06-08T15:23:16+08:00","date":"2026-06-08","gate":"audit","result":"PASS","detail":"2026-06-08-full-align-deploy.md"}
{"ts":"2026-06-08T15:23:16+08:00","date":"2026-06-08","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-08T15:23:16+08:00","date":"2026-06-08","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-08T15:23:16+08:00","date":"2026-06-08","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-08T15:23:16+08:00","date":"2026-06-08","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-08T15:23:16+08:00","date":"2026-06-08","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-08T15:23:16+08:00","date":"2026-06-08","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-08T15:23:16+08:00","date":"2026-06-08","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-08T15:23:16+08:00","date":"2026-06-08","gate":"summary","result":"PASS","detail":"7/7"}
{"ts":"2026-06-08T23:05:56+08:00","date":"2026-06-08","gate":"changelog","result":"PASS","detail":"2026-06-08-agent-401-stop-heartbeat.md 40lines"}
{"ts":"2026-06-08T23:05:56+08:00","date":"2026-06-08","gate":"audit","result":"PASS","detail":"2026-06-08-agent-401-stop-heartbeat.md"}
{"ts":"2026-06-08T23:05:56+08:00","date":"2026-06-08","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-08T23:05:56+08:00","date":"2026-06-08","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-08T23:05:56+08:00","date":"2026-06-08","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-08T23:05:56+08:00","date":"2026-06-08","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-08T23:05:56+08:00","date":"2026-06-08","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
{"ts":"2026-06-08T23:05:56+08:00","date":"2026-06-08","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-08T23:05:56+08:00","date":"2026-06-08","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-08T23:05:56+08:00","date":"2026-06-08","gate":"summary","result":"BLOCK","detail":"6/7"}
{"ts":"2026-06-08T23:06:09+08:00","date":"2026-06-08","gate":"changelog","result":"PASS","detail":"2026-06-08-agent-401-stop-heartbeat.md 40lines"}
{"ts":"2026-06-08T23:06:09+08:00","date":"2026-06-08","gate":"audit","result":"PASS","detail":"2026-06-08-agent-401-stop-heartbeat.md"}
{"ts":"2026-06-08T23:06:09+08:00","date":"2026-06-08","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-08T23:06:09+08:00","date":"2026-06-08","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-08T23:06:09+08:00","date":"2026-06-08","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-08T23:06:09+08:00","date":"2026-06-08","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-08T23:06:09+08:00","date":"2026-06-08","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-08T23:06:09+08:00","date":"2026-06-08","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-08T23:06:09+08:00","date":"2026-06-08","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-08T23:06:09+08:00","date":"2026-06-08","gate":"summary","result":"PASS","detail":"7/7"}
{"ts":"2026-06-08T23:19:12+08:00","date":"2026-06-08","gate":"changelog","result":"PASS","detail":"2026-06-08-beijing-time-display-unified.md 40lines"}
{"ts":"2026-06-08T23:19:12+08:00","date":"2026-06-08","gate":"audit","result":"PASS","detail":"2026-06-08-beijing-time-recovery-telegram.md"}
{"ts":"2026-06-08T23:19:12+08:00","date":"2026-06-08","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-08T23:19:12+08:00","date":"2026-06-08","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-08T23:19:12+08:00","date":"2026-06-08","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-08T23:19:12+08:00","date":"2026-06-08","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-08T23:19:12+08:00","date":"2026-06-08","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-08T23:19:12+08:00","date":"2026-06-08","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-08T23:19:12+08:00","date":"2026-06-08","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-08T23:19:12+08:00","date":"2026-06-08","gate":"summary","result":"PASS","detail":"7/7"}
{"ts":"2026-06-08T23:28:37+08:00","date":"2026-06-08","gate":"changelog","result":"PASS","detail":"2026-06-08-servers-hide-csv-export.md 17lines"}
{"ts":"2026-06-08T23:28:37+08:00","date":"2026-06-08","gate":"audit","result":"PASS","detail":"2026-06-08-push-validation-servers-ui.md"}
{"ts":"2026-06-08T23:28:37+08:00","date":"2026-06-08","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-08T23:28:37+08:00","date":"2026-06-08","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-08T23:28:37+08:00","date":"2026-06-08","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-08T23:28:37+08:00","date":"2026-06-08","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-08T23:28:37+08:00","date":"2026-06-08","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-08T23:28:37+08:00","date":"2026-06-08","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-08T23:28:37+08:00","date":"2026-06-08","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-08T23:28:37+08:00","date":"2026-06-08","gate":"summary","result":"PASS","detail":"7/7"}
{"ts":"2026-06-09T01:33:28+08:00","date":"2026-06-09","gate":"changelog","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-09T01:33:28+08:00","date":"2026-06-09","gate":"audit","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-09T01:33:28+08:00","date":"2026-06-09","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-09T01:33:28+08:00","date":"2026-06-09","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-09T01:33:28+08:00","date":"2026-06-09","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-09T01:33:28+08:00","date":"2026-06-09","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-09T01:33:28+08:00","date":"2026-06-09","gate":"review","result":"BLOCK","detail":"no audit file"}
{"ts":"2026-06-09T01:33:28+08:00","date":"2026-06-09","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-09T01:33:28+08:00","date":"2026-06-09","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-09T01:33:28+08:00","date":"2026-06-09","gate":"summary","result":"BLOCK","detail":"4/7"}
{"ts":"2026-06-09T01:34:25+08:00","date":"2026-06-09","gate":"changelog","result":"PASS","detail":"2026-06-09-full-site-testing-delivery.md 54lines"}
{"ts":"2026-06-09T01:34:25+08:00","date":"2026-06-09","gate":"audit","result":"PASS","detail":"2026-06-09-full-site-testing-delivery.md"}
{"ts":"2026-06-09T01:34:25+08:00","date":"2026-06-09","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-09T01:34:25+08:00","date":"2026-06-09","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-09T01:34:25+08:00","date":"2026-06-09","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-09T01:34:25+08:00","date":"2026-06-09","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-09T01:34:25+08:00","date":"2026-06-09","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
{"ts":"2026-06-09T01:34:25+08:00","date":"2026-06-09","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-09T01:34:25+08:00","date":"2026-06-09","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-09T01:34:25+08:00","date":"2026-06-09","gate":"summary","result":"BLOCK","detail":"6/7"}
{"ts":"2026-06-09T01:34:51+08:00","date":"2026-06-09","gate":"changelog","result":"PASS","detail":"2026-06-09-full-site-testing-delivery.md 54lines"}
{"ts":"2026-06-09T01:34:51+08:00","date":"2026-06-09","gate":"audit","result":"PASS","detail":"2026-06-09-full-site-testing-delivery.md"}
{"ts":"2026-06-09T01:34:51+08:00","date":"2026-06-09","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-09T01:34:51+08:00","date":"2026-06-09","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-09T01:34:51+08:00","date":"2026-06-09","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-09T01:34:51+08:00","date":"2026-06-09","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-09T01:34:51+08:00","date":"2026-06-09","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-09T01:34:51+08:00","date":"2026-06-09","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-09T01:34:51+08:00","date":"2026-06-09","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-09T01:34:51+08:00","date":"2026-06-09","gate":"summary","result":"PASS","detail":"7/7"}
{"ts":"2026-06-09T02:12:43+08:00","date":"2026-06-09","gate":"changelog","result":"PASS","detail":"2026-06-09-full-site-testing-delivery.md 58lines"}
{"ts":"2026-06-09T02:12:43+08:00","date":"2026-06-09","gate":"audit","result":"PASS","detail":"2026-06-09-full-site-testing-delivery.md"}
{"ts":"2026-06-09T02:12:43+08:00","date":"2026-06-09","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-09T02:12:43+08:00","date":"2026-06-09","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-09T02:12:43+08:00","date":"2026-06-09","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-09T02:12:43+08:00","date":"2026-06-09","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-09T02:12:43+08:00","date":"2026-06-09","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-09T02:12:43+08:00","date":"2026-06-09","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-09T02:12:43+08:00","date":"2026-06-09","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-09T02:12:43+08:00","date":"2026-06-09","gate":"summary","result":"PASS","detail":"7/7"}
+14 -3
View File
@@ -49,7 +49,18 @@
---
## 4. 设计与标准
## 4. 测试(全站用例 SSOT
| 文档 | 说明 |
|------|------|
| [testing/test-case-matrix.md](testing/test-case-matrix.md) | **152 条** UT/IT/CH/E2E/MCP 编号用例 |
| [testing/mcp-browser-playbook.md](testing/mcp-browser-playbook.md) | Agent Playwright MCP 点页规程 |
| [design/specs/2026-06-08-full-site-testing-strategy-design.md](design/specs/2026-06-08-full-site-testing-strategy-design.md) | 四层金字塔设计 |
| [design/plans/2026-06-08-full-site-testing-strategy.md](design/plans/2026-06-08-full-site-testing-strategy.md) | 分阶段实施计划 |
---
## 5. 设计与标准
| 目录 | 说明 |
|------|------|
@@ -60,13 +71,13 @@
---
## 5. 归档区
## 6. 归档区
**[archive/README.md](archive/README.md)** — 2026-06 审计合并卷、旧 handoff、agentmemory、2026-05 审查报告。
---
## 6. 工具脚本
## 7. 工具脚本
| 脚本 | 用途 |
|------|------|
@@ -0,0 +1,66 @@
# 审计 — 全站测试体系交付(2026-06-09
**Changelog**: `docs/changelog/2026-06-09-full-site-testing-delivery.md`
## 变更文件清单
| 文件 | 说明 |
|------|------|
| `tests/integration/test_*.py` | IT 域集成测试(14 文件) |
| `tests/chain/test_*.py` | 链路测试(8 文件,含 batch/exec 完成态) |
| `tests/conftest.py` | 共享 fixture / SQLite 兼容 |
| `tests/acceptance_catalog.py` | 验收目录扩展 |
| `frontend/e2e/fixtures.mjs` | Worker 级 API 登录 + cookie 轮换写回 |
| `frontend/e2e/helpers.mjs` | 会话恢复 + 侧栏 `nav` |
| `frontend/e2e/pages/*.spec.mjs` | 页域 E2E18 |
| `frontend/e2e/full-acceptance.spec.mjs` | 15 步 serial 验收 |
| `frontend/package.json` | `test:e2e` 脚本 |
| `.gitea/workflows/ci-cd.yml` | CI 分层 pytest + E2E job |
| `.gitignore` | 忽略 `frontend/e2e/.auth/``test-results/` |
| `scripts/run_nexus_acceptance.py` | 验收脚本对齐 catalog |
| `docs/testing/test-case-matrix.md` | 矩阵状态更新 |
| `web/app/index.html` | vite build 入口 hash 更新 |
| `frontend/src/api/index.ts` | 校验错误中文格式化接入 |
| `frontend/src/utils/apiError.ts` | API 错误展示 |
| `frontend/src/utils/validationErrorFormat.ts` | Pydantic detail → 中文 |
| `frontend/src/pages/PushPage.vue` | 推送页异步提交 UX |
| `frontend/src/pages/ServersPage.vue` | 服务器列表(无 CSV 导出) |
| `frontend/src/components/push/PushToolbar.vue` | 推送工具栏 |
| `frontend/src/composables/push/usePushProgress.ts` | 推送进度 composable |
| `server/api/sync_v2.py` | sync 校验与异步推送 |
| `server/background/sync_push_runner.py` | 后台推送 runner |
| `server/utils/validation_errors_zh.py` | 后端校验中文映射 |
| `tests/test_validation_errors_zh.py` | 中文校验单测 |
## Step 3 规则扫描
| 规则 | 结论 |
|------|------|
| 无静默吞错(测试 assert 真实行为) | PASS — 失败用例已修至 green |
| 无明文密钥入仓 | PASS — E2E 密码仅环境变量 |
| 分层:API 不测 UI、E2E 不替代 IT | PASS — 职责分离 |
| E2E 并行不触发 login lockout | PASS — `fixtures.mjs` 每 worker 一次 API 登录 |
| Refresh token 轮换 | PASS — 上下文结束写回 `storageState` |
| SQLite/MySQL 测试兼容 | PASS — `SettingRepository` 等 monkeypatch/直连 |
| CI 不阻塞于缺 E2E secret | PASS — E2E job 条件触发 |
| 推送异步不阻塞 UI / 校验中文 | PASS — sync_v2 + PushPage + validation_errors_zh |
## Closure
| 项 | 结果 |
|----|------|
| pytest integration+chain | 27 passed |
| pytest fast suite | 427 passed |
| pytest slow (IT-INST) | 1 passed |
| Playwright e2e | 35 passed |
| local_integration_smoke | OK |
| pre_deploy_check Gate 36 | PASS(本审计前) |
| MCP-INST-001 各 step UI | ⏭ 用户确认不纳入验收 |
## DoD
- [x] Wave 15 矩阵 IT/CH/E2E 缺口落地并有命令证据
- [x] Playwright 35/35 本地 green(清理 assets 后复验)
- [x] Changelog + 设计/计划/报告文档齐全
- [x] 门控可过(今日 changelog + 本审计)
- [ ] 生产部署(待用户批准 `deploy-production.sh`
@@ -0,0 +1,44 @@
# 2026-06-08 告警中心 / 命令日志 / 调度契约修复
## 摘要
修复告警中心统计与筛选、命令日志表格恒空、调度列表脚本列空白等前后端 API 契约不一致问题。
## 动机
全站 E2E 仅验证页面可打开,未覆盖真实查询参数与响应结构;生产上三页表现为数据为空、统计恒 0、筛选无效。
## 变更
### 告警中心(`server/api/settings.py`
- `GET /api/alert-history/`:支持前端 `page`/`per_page``type`(→ `alert_type`)、`status``active`/`recovered``is_recovery`);响应补充 `page`/`per_page`/`pages`
- `GET /api/alert-history/stats`:补充 `today`/`active`/`recovered`/`top_server``active` 取自 Redis `alerts:*` 集合基数;`today` 按当日日期匹配 `daily`;日统计 SQL 改为 SQLAlchemy 可移植查询(兼容测试 SQLite)。
### 命令日志(`server/api/assets.py`
- `GET /api/assets/command-logs``/ssh-sessions`:由裸数组改为 `{ items, total, page, per_page, pages }`,与 `fetchPagePerPage` 一致。
### 调度(`frontend/src/pages/SchedulesPage.vue`
- 列表「源文件 / 脚本」列:脚本类型显示脚本库名称或命令摘要,不再空白。
## 涉及文件
- `server/api/settings.py`
- `server/api/assets.py`
- `frontend/src/pages/SchedulesPage.vue`
- `tests/integration/test_alerts_audit.py`
- `tests/integration/test_retries_commands.py`
## 迁移 / 重启
- 无需 DB 迁移;需重启 API 并重新构建前端后部署。
## 验证
```bash
.venv/bin/pytest tests/integration/test_alerts_audit.py tests/integration/test_retries_commands.py -q
bash scripts/local_verify.sh
cd frontend && npm run build
```
@@ -0,0 +1,29 @@
# 2026-06-08 — 全站测试体系规划
## 摘要
新增四层测试金字塔(单元 / 集成 / 链条 / UI E2E+ Agent MCP 浏览器探索层的设计、实施计划与 **152 条编号用例矩阵**
## 动机
用户要求全站补齐单元、集成、链条测试,并由 Agent 通过 MCP 在页面上执行可重复点页用例;现有 ~60 pytest 与 15 条 Playwright 冒烟缺少系统编号与链条覆盖。
## 涉及文件
- `docs/design/specs/2026-06-08-full-site-testing-strategy-design.md`
- `docs/design/plans/2026-06-08-full-site-testing-strategy.md`
- `docs/testing/test-case-matrix.md`
- `docs/testing/mcp-browser-playbook.md`
## 迁移/重启
无代码变更,无需部署。
## 验证
- 文档内命令与现有 `scripts/run_nexus_acceptance.py``frontend/e2e/full-acceptance.spec.mjs` 路径一致
- 用例矩阵与现有 `tests/test_*.py` 映射已标注 ✅/🔲
## 后续
按 Phase 15 实施:先补 `E2E-SRV-002``IT-PUSH-001``CH-PUSH-001`,再拆 `tests/unit` 目录。
@@ -0,0 +1,42 @@
# Changelog — Playwright E2E 35/35 + worker 会话复用
**日期**: 2026-06-08
## 摘要
修复并行 E2E 登录锁号与 SPA 导航不稳定,全量 **35 passed / 0 failed**
## 动机
- 35 个 Playwright 用例并行跑时每个都走 UI 登录,触发 `login_attempts` 锁定(429)。
- Refresh token 轮换后静态 `storageState` 文件失效,后续用例落在 `#/login``#/`
- Hash-only `page.goto` 与 Vue keep-alive 组合下,部分用例无法离开仪表盘。
## 涉及文件
| 路径 | 变更 |
|------|------|
| `frontend/e2e/fixtures.mjs` | 新增:worker 级 API 登录 + 上下文结束写回 cookie |
| `frontend/e2e/helpers.mjs` | `login` 仅恢复会话;`nav` 侧栏点击导航 |
| `frontend/e2e/pages/*.spec.mjs` | 认证用例改 import `fixtures.mjs`auth/login 空 `storageState` |
| `frontend/e2e/pages/credentials.spec.mjs` | 凭据对话框经「凭据」按钮打开 |
| `frontend/e2e/full-acceptance.spec.mjs` | 凭据/命令日志选择器对齐 |
| `frontend/playwright.config.mjs` | 移除全局 `storageState`(改由 fixture |
| `.gitignore` | 忽略 `frontend/e2e/.auth/` |
| `docs/reports/2026-06-08-playwright-e2e-full-run.md` | 35/35 证据 |
## 迁移 / 重启
- 无需 DB 迁移。
- E2E 前若 admin 已锁定,需清理 `login_attempts` 或等待 15 分钟。
- 改前端后需 `cd frontend && npx vite build` 再跑 E2E(测的是 `web/app/`)。
## 验证
```bash
cd frontend && NEXUS_E2E_BASE=http://127.0.0.1:8600 NEXUS_E2E_PASSWORD=*** npm run test:e2e
# → 35 passed
.venv/bin/pytest tests/integration tests/chain -q
# → 27 passed
```
@@ -0,0 +1,26 @@
# 2026-06-08 — Playwright 全量 E2E 32/33 通过
## 摘要
本地 `npm run test:e2e` 首次在扩展后的 19 个 spec 目录下跑通:**32 passed / 1 skipped**~42s)。修复 6 处选择器与已移除的全局搜索用例。
## 动机
Wave 5 仅完成 pytest;用户要求用真实 admin 凭据补跑 Playwright 全量并留证据。
## 涉及文件
- `frontend/e2e/full-acceptance.spec.mjs`
- `frontend/e2e/pages/{credentials,executions,executions-detail,retries,push}.spec.mjs`
- `docs/reports/2026-06-08-playwright-e2e-full-run.md`
## 迁移/重启
无。首次环境需 `npx playwright install chromium`
## 验证
```bash
# API :8600 已起,凭据经环境变量传入(勿写进仓库)
cd frontend && NEXUS_E2E_BASE=http://127.0.0.1:8600 NEXUS_E2E_PASSWORD=*** npm run test:e2e
```
@@ -0,0 +1,57 @@
# 2026-06-08 — 全站测试用例 Wave 14 落地
## 摘要
按计划补齐 IT/CH/E2E 自动化:推送 accepted、链条 WS、12 条集成测试、5 条链条测试、14 条 Playwright 页面 specCI 分层跑 pytest;验收脚本支持 `--with-chain` / `--with-integration`
## 动机
落实 `docs/testing/test-case-matrix.md` 中 ~78 条待写用例,MCP 可自动化项沉淀为 Playwright(可 CI)。
## 涉及文件
### 后端
- `tests/conftest.py` — fixtures、`integration_env``mock_request`
- `tests/integration/test_*.py`7 文件,12 用例)
- `tests/chain/test_*.py`5 文件)
- `tests/test_batch_server_display_name.py` — 修正 import(CI 收集错误)
### 前端 E2E
- `frontend/e2e/helpers.mjs`
- `frontend/e2e/pages/*.spec.mjs`11 spec
- `frontend/e2e/full-acceptance.spec.mjs` — 复用 helpers
- `frontend/package.json``test:e2e` 跑整个 `e2e/`
### CI / 验收
- `.gitea/workflows/ci-cd.yml` — fast pytest + chain job
- `scripts/run_nexus_acceptance.py``--with-chain``--with-integration`、Playwright 全目录
- `tests/acceptance_catalog.py` — integration/chain/ui_executions
### 文档
- `docs/testing/test-case-matrix.md` — 状态更新
## 迁移/重启
无 DB 迁移。CI 需已安装 `pytest-asyncio`
## 验证
```bash
.venv/bin/pytest tests/integration tests/chain -q
.venv/bin/pytest tests/ -m "not chain and not slow" -q # 注意既有 test_batch_server_display_name 收集错误需另修
cd frontend && NEXUS_E2E_BASE=http://127.0.0.1:8600 NEXUS_E2E_PASSWORD=*** npm run test:e2e
python scripts/run_nexus_acceptance.py --with-integration --with-chain
```
## 续补(同日前半)
- `tests/integration/test_webssh_auth.py` — IT-TERM-001
- `tests/integration/test_schedules.py` — IT-SCH-001
- `tests/chain/test_agent_dashboard_flow.py` — CH-AGT-001
- `tests/chain/test_server_update_flow.py` — CH-SRV-001
- `frontend/e2e/pages/login.spec.mjs` — MCP-AUTH-001
- `.gitea/workflows/ci-cd.yml` — main 分支可选 E2E job
## 剩余 manual-only
`MCP-TERM-001`(真 SSH)、`MCP-INST-*``CH-BAT-001``IT-INST-001` 等。
@@ -0,0 +1,48 @@
# 2026-06-08 — 测试用例 Wave 5(缺口补齐)
## 摘要
补齐矩阵中 CH-BAT-001、IT-FILE/INST/SRV-003 及 7 条 MCP→E2E 沉淀;集成/链条测试 26 条全绿,fast pytest 427 passed。
## 动机
接续 Wave 14,关闭 `test-case-matrix.md` 中批量任务、文件浏览、安装模式、notify 开关及终端/文件/脚本/凭据/重试/设置/执行详情等 MCP 可自动化缺口。
## 涉及文件
### 后端
- `tests/chain/test_batch_job_flow.py` — CH-BAT-001
- `tests/integration/test_files_browse.py` — IT-FILE-001
- `tests/integration/test_notify_settings.py` — IT-SRV-003
- `tests/integration/test_install_mode.py` — IT-INST-001`@pytest.mark.slow`
### 前端 E2E
- `frontend/e2e/pages/terminal.spec.mjs`
- `frontend/e2e/pages/files.spec.mjs`
- `frontend/e2e/pages/scripts.spec.mjs`
- `frontend/e2e/pages/credentials.spec.mjs`
- `frontend/e2e/pages/retries.spec.mjs`
- `frontend/e2e/pages/settings.spec.mjs`
- `frontend/e2e/pages/executions-detail.spec.mjs`
### 文档
- `docs/testing/test-case-matrix.md` — 状态与覆盖率汇总
## 迁移/重启
无。
## 验证
```bash
.venv/bin/pytest tests/integration tests/chain -q # 26 passed
.venv/bin/pytest tests/ -m "not chain and not slow" -q # 427 passed
# E2E(需本地 API + NEXUS_E2E_PASSWORD:
cd frontend && NEXUS_E2E_BASE=http://127.0.0.1:8600 NEXUS_E2E_PASSWORD=*** npm run test:e2e
```
## 仍待写(manual / 安装隔离)
- E2E-INST-001 / MCP-INST-001 — 安装模式专用环境
- CH-EXEC-001 — 轮询至 `done`(需 mock runner
- MCP-PUSH-001 / MCP-SET-001 toast — 部分已由 E2E mock 覆盖,完整 toast 需 route mock
@@ -0,0 +1,58 @@
# Changelog — 全站测试体系交付(Wave 15 + E2E 35/35
**日期**: 2026-06-09
## 摘要
补齐 `test-case-matrix` 中 IT/CH/E2E 缺口;Playwright 全量 35/35 greenpytest 455 passed(含 slow)。
## 动机
- Wave 15 仅有 pytest 证据,缺 Playwright 全量与并行稳定性。
- 并行 UI 登录导致 admin 锁定;`web/app/assets` 陈旧 hash 堆积(清理前 7086 文件 / 491MB)。
## 后端测试(新增/扩展)
| 域 | 路径 |
|----|------|
| Integration | `tests/integration/test_*.py`14 文件,含 IT-INST/FILE/SRV-003 等) |
| Chain | `tests/chain/test_*.py`8 文件,含 CH-BAT/CH-EXEC 完成态) |
| Catalog | `tests/acceptance_catalog.py` |
## 前端 E2E
| 项 | 路径 |
|----|------|
| Worker 会话 | `frontend/e2e/fixtures.mjs` |
| 导航助手 | `frontend/e2e/helpers.mjs`(侧栏点击,避免 keep-alive 卡仪表盘) |
| 页域 spec | `frontend/e2e/pages/*.spec.mjs`18+ `full-acceptance.spec.mjs` |
| 配置 | `frontend/playwright.config.mjs``package.json` `test:e2e` |
## CI
- `.gitea/workflows/ci-cd.yml`:分层 pytest + 可选 Playwright job
## 文档
- `docs/testing/test-case-matrix.md`
- `docs/design/specs|plans/2026-06-08-full-site-testing-strategy*.md`
- `docs/reports/2026-06-08-playwright-e2e-full-run.md`
## 迁移 / 重启
- 无 DB 迁移。
- E2E 需 `NEXUS_E2E_PASSWORD` + API :8600;改前端后 `npx vite build`
## 验收范围调整(2026-06-09
- **MCP-INST-001**(安装向导各 step UI):用户确认**不验收**;矩阵标 `⏭`;保留 E2E-INST-001 静态页可达 + UT/IT 安装锁即可。
## 验证(2026-06-09 复验)
```bash
pytest tests/integration tests/chain -q # 27 passed
pytest tests/ -m "not chain and not slow" -q # 427 passed
pytest tests/ -m slow -q # 1 passed
cd frontend && npm run test:e2e # 35 passed
bash deploy/pre_deploy_check.sh # 目标 7/7
```
@@ -0,0 +1,78 @@
# 全站测试实施计划(2026-06-08
> 设计:`docs/design/specs/2026-06-08-full-site-testing-strategy-design.md`
> 用例 SSOT`docs/testing/test-case-matrix.md`
> MCP 规程:`docs/testing/mcp-browser-playbook.md`
## 现状快照
| 资产 | 数量/说明 |
|------|-----------|
| pytest 文件 | ~60(根目录 `tests/test_*.py` |
| 门控 | `pre_deploy_check``tests/test_api.py` |
| UI E2E | `frontend/e2e/full-acceptance.spec.mjs`15 caseserial |
| 验收聚合 | `scripts/run_nexus_acceptance.py` + `acceptance_catalog.py` |
| 前端单测 | 无 vitest |
| MCP | Playwright MCP + MySQL MCP 已配置 |
## 实施阶段
### Phase 1 — 文档与编号(本周)
1. 发布 `test-case-matrix.md`(本文档姊妹篇)
2. 发布 `mcp-browser-playbook.md`
3. `acceptance_catalog.py` 增加 `chain` layer 与 `executions`
### Phase 2 — 目录与标记(12 周)
1. 新增 `tests/unit/``tests/integration/``tests/chain/`**新用例只写新目录**
2. `pytest.ini``pyproject` 注册 markers`unit`, `integration`, `chain`, `slow`
3. 从现有文件**复制**(非搬家)高价值用例各 1 例作为模板
### Phase 3 — 集成层现代化(2 周)
1. `tests/integration/conftest.py``AsyncClient` + 测试 DB 事务回滚
2.`test_api.py` 断言逐步迁为 pytest(保留脚本作门控兼容层)
3. 契约:关键 API 响应 schema 快照(Pydantic model dump
### Phase 4 — 链条 + UI 拆分(23 周)
1. `tests/chain/test_push_async_flow.py`POST accepted → WS progress mock
2. `tests/chain/test_script_exec_flow.py`:创建脚本 → 执行 → 看板状态
3. 拆分 `full-acceptance.spec.mjs``frontend/e2e/pages/*.spec.mjs`
4. 新增 `flows/push-submit.spec.mjs`(非破坏性)
### Phase 5 — CI 与 MCP 闭环(持续)
1. `.gitea/workflows/ci-cd.yml` 增加 `pytest -m "not chain and not slow"`
2. nightly`run_nexus_acceptance.py --with-unit --with-ui`
3. 每个 sprint 选 3 条 `MCP-*` 沉淀为 Playwright
## 本地命令速查
```bash
# L1 单元
pytest tests/unit tests/test_posix_paths.py -q -m unit
# L2 集成(需 docker + start-dev
pytest tests/integration -q -m integration
# L3 链条
pytest tests/chain -q -m chain
# L4 UI
cd frontend && NEXUS_E2E_BASE=http://127.0.0.1:8600 NEXUS_E2E_PASSWORD=*** npm run test:e2e
# 全量验收
python scripts/run_nexus_acceptance.py --with-unit --with-ui
```
## 回滚
- 新目录与 marker 均为增量;删除 `tests/unit` 等不影响现门控
- `test_api.py` 在迁移完成前保持不动
## 测试要点(实施自检)
- [ ] 任意新功能 PR 必须标明影响的用例编号(如 `IT-PUSH-003`
- [ ] MCP 探索报告路径:`docs/reports/mcp-runs/YYYY-MM-DD-<id>.md`
@@ -0,0 +1,90 @@
# 全站测试体系设计(2026-06-08
## 背景与目标
Nexus 6.0 已有 **~60 个 pytest 模块**、`tests/test_api.py` 门控脚本、`frontend/e2e/full-acceptance.spec.mjs`15 条 UI 冒烟)、`scripts/run_nexus_acceptance.py` 统一验收入口。缺口在于:
- 单元测试按**功能域**未全覆盖,前端几乎无单测
- 集成测试以手写 `urllib` 为主,缺契约快照与 DB 夹具规范
- **链条测试**(多 API / 多页面 / WS / 后台任务)用例未系统化
- Agent **MCP 浏览器点页**有 Playwright MCP,但未沉淀为可重复用例清单
**目标**:建立四层测试金字塔 + Agent MCP 探索层,全部有**编号用例**、可 CI、可 `run_nexus_acceptance.py` 汇总。
## 方案对比
| 方案 | 优点 | 缺点 | 结论 |
|------|------|------|------|
| A. 仅扩 pytest | 与现门控一致 | 无真实 UI 交互 | 作为 L1–L3 底座 |
| B. 仅 Playwright | 贴近用户 | 慢、脆、难测纯逻辑 | 作为 L4 |
| C. MCP 临时点页 | Agent 灵活 | 不可 CI、难回归 | 作为 L5 探索 + 用例沉淀到 B |
| **D. 四层 + MCP 规程(选定)** | 分工清晰、可渐进 | 初期文档/用例工作量大 | ✅ |
## 选定架构
```
┌─────────────────────┐
│ L5 Agent MCP 探索 │ 新功能/回归抽测 → 沉淀为 L4
└──────────┬──────────┘
┌──────────▼──────────┐
│ L4 UI E2E (Playwright)│ frontend/e2e/*.spec.mjs
└──────────┬──────────┘
┌──────────▼──────────┐
│ L3 链条 (Chain) │ tests/chain/ + pytest markers
└──────────┬──────────┘
┌──────────▼──────────┐
│ L2 集成 (API+DB) │ tests/integration/ + test_api.py
└──────────┬──────────┘
┌──────────▼──────────┐
│ L1 单元 (Unit) │ tests/unit/ 按域拆分
└─────────────────────┘
```
### 工具链
| 层 | 工具 | CI | Agent MCP |
|----|------|-----|-----------|
| L1 | pytest + pytest-asyncio + 内存 SQLite/假 Redis | ✅ gitea workflow | 否 |
| L2 | pytest + httpx AsyncClient + docker mysql/redis | ✅ 可选 job | MySQL MCP 验数 |
| L3 | pytest markers `@pytest.mark.chain` | ✅ nightly | 否 |
| L4 | `@playwright/test` | ✅ `--with-ui` | Playwright MCP 对照用例 |
| L5 | Playwright MCP `browser_*` | ❌ 人工触发 Agent | ✅ |
### 目录规划(渐进迁移,不一次性搬空 tests/)
```
tests/
unit/ # 纯函数、schema、utils(从现有 test_*.py 归类迁入)
integration/ # API + DBhttpx TestClient / 活 API
chain/ # 多步业务流
fixtures/ # 共享 conftest 分层
acceptance_catalog.py # 扩展 layer: unit|integration|chain|ui|mcp
frontend/e2e/
pages/ # 每页一个 spec(从 full-acceptance 拆分)
flows/ # 链条:登录→推送→进度→历史
docs/testing/
test-case-matrix.md # 全量用例 SSOT(编号)
mcp-browser-playbook.md # Agent MCP 点页规程
```
### MCP Agent 点页规程(原则)
1. **用例先行**:每个 MCP 探索必须对应 `docs/testing/test-case-matrix.md``MCP-*` 编号
2. **步骤可复现**snapshot → click → assert 文本;截图存 `docs/reports/e2e-screenshots/`
3. **禁止生产破坏性操作**:推送/脚本执行/批量删除仅对 `test-*` 子机或 dry-run
4. **沉淀**:MCP 通过的流程 7 日内写入 Playwright specL4
### 安全与性能约束
- 测试账号:`NEXUS_TEST_ADMIN_*`,禁止写生产密钥入 git
- E2E 默认 `127.0.0.1:8600`;生产 URL 仅只读冒烟且需用户批准
- 链条测试子机:标记 `is_test=1` 或使用 ID 段约定(文档化)
- 单 CI job pytest 目标 < 10minchain/ui 放 nightly
## 验收标准(体系本身)
- [ ] `docs/testing/test-case-matrix.md` 覆盖 15 SPA 页 + 安装向导 + Agent 心跳
- [ ] 每层有 ≥1 个可运行示例用例与命令
- [ ] `run_nexus_acceptance.py` 能 rollup unit/integration/chain/ui 四层
- [ ] Agent 手册 `mcp-browser-playbook.md` 含登录、推送提交、脚本执行看板 3 条标准 MCP 流
- [ ] CIPR 跑 L1+L2main nightly 跑 L3+L4
@@ -0,0 +1,33 @@
# 2026-06-08 Agent 401 停心跳 — 部署验证
## 部署信息
| 项 | 值 |
|----|-----|
| Commit | `972456a` feat(agent): 401 停心跳、配置重载后重启与升级双文件下发 |
| 门控 | 7/7 PASS |
| 方式 | `deploy/deploy-production.sh` |
## 生产检查
| 检查 | 结果 |
|------|------|
| `GET /health` | `ok` |
| `GET /app/` | HTTP 200 |
| `GET /agent/heartbeat_policy.py` | HTTP 200,含 `heartbeat_should_stop` |
| `GET /agent/agent.py` | 含 `heartbeat_policy` import、`AGENT_VERSION` |
## 子机 rollout#7 · 用户/运维)
中心已提供新文件;**各子机须批量「升级 Agent」** 才会下载 `agent.py` + `heartbeat_policy.py`
验收步骤(抽 1 台):
1. `journalctl -u nexus-agent -f` 观察心跳
2. 临时改错 `config.json` 中 central api_key → 应出现 **一条** 401 error 后不再重复
3. 通过管理端修正 key 或 `/config/reload` → 心跳应恢复
## 回滚
MySQL: `/var/backups/nexus/nexus-before-upgrade-20260608-150632.sql.gz`
镜像: `nexus-prod-nexus:rollback`
@@ -0,0 +1,40 @@
# 部署验证 — 北京时间统一 + 恢复 Telegram 去重(2026-06-08
## 部署信息
| 项 | 值 |
|----|-----|
| Commit | `abeab9d` |
| 消息 | Fix recovery Telegram dedup and unify Beijing time display |
| 路径 | `/opt/nexus` Docker |
| 时间 | 2026-06-08 |
## 门控
`pre_deploy_check.sh`**7/7 PASS**
## 健康检查
| 检查 | 结果 |
|------|------|
| `https://api.synaglobal.vip/health` | `ok` |
| `https://api.synaglobal.vip/app/` | HTTP 200 |
| 远程 `127.0.0.1:8600/health` | ok |
| MySQL 升级前备份 | `nexus-before-upgrade-20260608-151933.sql.gz` |
## 生产代码抽检
- `format_beijing_now()` 输出含「北京时间」
- `_detect_recovery` 对 legacy `mem:90` + `mem:92` 仅返回 `[('mem', 50.0)]`
## 用户终验建议
1. **审计页** `#/audit` — 「时间」列应为北京时间(非 UTC 裸字符串)
2. **告警历史** `#/alerts` — 同上
3. 子机内存超阈值后回落 — Telegram **每条指标仅 1 条**恢复消息,时间为北京时间
## 进度
```
☑实现 ☑本地验证 ☑审计8步 ☑部署 ☑健康检查 □浏览器验证(待用户终验) ☑changelog
```
@@ -0,0 +1,52 @@
# 2026-06-08 全量对齐部署验证
## 部署信息
| 项 | 值 |
|----|-----|
| Commit | `69068e2` feat(ops): server_batch 回收、sync_logs 清理与 Agent 安装跳过 |
| 前序 | `a401ea5`Settings 422、server_ids 2000 |
| 方式 | `deploy/deploy-production.sh` → Docker `nexus-1panel.sh upgrade` |
| 时间 | 2026-06-08 |
## 门控
```
pre_deploy_check.sh → 7/7 PASS
pytest test_agent_version + test_server_batch_reconcile + test_sync_log_purge → 16 passed
local_verify → 26/26
```
## 生产健康
| 检查 | 结果 |
|------|------|
| `GET https://api.synaglobal.vip/health` | `ok` |
| `GET https://api.synaglobal.vip/app/` | HTTP 200 |
| 容器 `recover_orphaned` in main.py | 2 |
| 容器 `sync_log_purge` in main.py | 5 |
| 容器 `agent_install_skip_result` | 2 |
| `server/utils/agent_version.py` | 存在 |
## 前端 bundle
| 位置 | index hash |
|------|------------|
| 生产 `/app/index.html` | `index-E6hwgODJ.js` |
| Docker 构建 | frontend 层 CACHE(源码未变,与上次镜像一致) |
说明:本次 commit 仅后端;Settings/调度等前端已在 `a401ea5` 源码中,当前 bundle 与升级前相同属预期。若需强制刷新 bundle,需 `docker build --no-cache` 或改动 `frontend/` 触发重建。
## 功能验收(Agent 终验)
- [ ] 批量安装 Agent:已装 2.0.0 子机应显示「已跳过」
- [ ] API 重启后 running 批量任务应收尾(僵尸 #20 类)
- [ ] 强刷浏览器后 Settings 保存无 422
## 回滚
```bash
ssh nexus 'sudo docker tag nexus-prod-nexus:rollback nexus-prod-nexus:latest && cd /opt/nexus && sudo nx update'
```
MySQL 备份:`/var/backups/nexus/nexus-before-upgrade-20260608-072350.sql.gz`
@@ -15,6 +15,8 @@
| 项 | 结果 |
|----|------|
| GET /health | |
| 仪表盘无服务器列表 | |
| Settings 关 CPU → 无恢复 Telegram | |
| GET https://api.synaglobal.vip/health | ok |
| GET /app/ | HTTP 200 |
| 生产 commit | cb49fd9 |
| 仪表盘无服务器列表 | 待浏览器终验 |
| Settings 关 CPU → 无恢复 Telegram | 待浏览器终验 |
@@ -0,0 +1,30 @@
# 2026-06-08 — notify 开关 + 行内详情 部署验收
## 版本
- **Commit**: `27da842`
- **生产**: Docker @ `/opt/nexus`
- **时间**: 2026-06-08
## 门控
- pre_deploy_check: **7/7 PASS**
- pytest `test_notify_toggle_sync.py`: **25 passed**
## 健康检查
| 项 | 结果 |
|----|------|
| `https://api.synaglobal.vip/health` | ok |
| `https://api.synaglobal.vip/app/` | 200 |
| 容器 | `nexus-prod-nexus-1` healthy |
## 变更摘要
1. **Telegram 告警开关** — Redis `nexus:settings` 跨 worker 同步;关闭 CPU/内存/磁盘等 notify_* 后不再发 TelegramWebSocket/日志仍记录)。
2. **服务器行内详情** — 点击名称展开 `ServerInlineDetail`expanded id 与 `item-value` 数字一致)。
## 浏览器终验(用户)
- [ ] 服务器页 → 点击名称 → 展开 CPU/内存/同步日志
- [ ] 设置页 → 关闭「CPU 超阈值告警」→ 确认不再收到 TelegramWebSocket 告警仍可有)
@@ -0,0 +1,53 @@
# Playwright 全量 E2E — 本地验证报告
**日期**: 2026-06-08
**环境**: `http://127.0.0.1:8600` · admin + `NEXUS_E2E_PASSWORD` · Chromium headless
## 结果(最终)
```bash
cd frontend && npx playwright install chromium # 首次
NEXUS_E2E_BASE=http://127.0.0.1:8600 \
NEXUS_E2E_USER=admin \
NEXUS_E2E_PASSWORD=*** \
npm run test:e2e
```
| 指标 | 值 |
|------|-----|
| 合计 | **35** |
| 通过 | **35** |
| 失败 | 0 |
| 跳过 | 0 |
| 耗时 | ~35s |
### 复验(清理后 · 2026-06-08
| 套件 | 结果 |
|------|------|
| `pytest tests/integration tests/chain` | **27 passed** |
| `pytest tests/ -m "not chain and not slow"` | **427 passed** |
| `pytest tests/ -m slow` | **1 passed**IT-INST-001 |
| `scripts/local_integration_smoke.sh` | **OK** |
| `npm run test:e2e` | **35 passed** (~35s) |
前置:`bash scripts/start-dev.sh`API :8600 healthy
## 根因与修复(本轮)
| 问题 | 处理 |
|------|------|
| 并行 UI 登录触发 admin 锁定(429 | `e2e/fixtures.mjs`:每 worker 一次 API 登录 + 轮换后更新 `storageState` |
| `page.goto(#/route)` 在 keep-alive SPA 下偶发停在 `#/` | `helpers.nav` 改为侧栏链接点击;设置页点抽屉「设置」 |
| 凭据入口改为服务器页对话框 | `credentials` / acceptance #08 点「凭据」按钮,不再依赖 `?credentials=1` |
| `getByText('命令日志')` strict 双匹配 | 限定 `main .v-card-title` |
| `web/app` 构建滞后(旧 bundle 含「导出CSV」) | `npx vite build` 同步源码与静态资源 |
## Spec 目录(19 文件)
- `full-acceptance.spec.mjs`15 步 serial
- `pages/*.spec.mjs`18 页域用例 + install/login/auth 无会话)
## 结论
**Wave 15 测试矩阵对应的 Playwright 全量 35/35 green**,可与 `tests/integration` + `tests/chain`27 passed)一并作为 L4 证据。
@@ -0,0 +1,13 @@
# 部署验证 — 推送后台化 + 422 中文化 + 隐藏 CSV2026-06-08
| 项 | 值 |
|----|-----|
| Commit | `091fb97` |
| 健康 | `https://api.synaglobal.vip/health` → ok |
| SPA | `/app/` → 200 |
## 终验要点
1. **推送页** — 点「推送」后按钮仅短暂 loading,出现「推送已提交」toast,下方进度列表继续更新
2. **服务器页** — 工具栏无「导出 CSV」
3. **422 提示** — 若参数超限,应显示「每页条数:不能大于 200」而非 `query.per_page`
@@ -0,0 +1,22 @@
# 2026-06-08 — 调度「当天」单次 UI 部署验证
## 部署
| 项 | 值 |
|----|-----|
| Commit | `22d5d21` |
| 门控 | 7/7 PASS |
| 容器 | `nexus-prod-nexus-1` healthy |
| /health | ok |
| /app/ | 200 |
## 前端产物
- `SchedulesPage-eYsxbL9M.js`(镜像内 vite build
- 含「当天」标签,单次模式无「重复方式」下拉
## 浏览器终验
1. 打开 https://api.synaglobal.vip/app/#/schedules
2. 新建/编辑调度 → 单次:应见「当天」+ 时/分,无「重复方式」
3. 切换循环:应见窄「重复方式」下拉,首项为「当天」
@@ -0,0 +1,29 @@
# Wave 5 测试缺口补齐 — 验证报告
**日期**: 2026-06-08
**范围**: CH-BAT-001、IT-FILE/INST/SRV-003、MCP→E2E 沉淀 7 页
## 本地 pytest
| 命令 | 结果 |
|------|------|
| `pytest tests/integration tests/chain -q` | **26 passed** |
| `pytest tests/ -m "not chain and not slow" -q` | **427 passed**, 9 deselected |
## 新增用例 ID
| ID | 实现 |
|----|------|
| CH-BAT-001 | `tests/chain/test_batch_job_flow.py` |
| IT-FILE-001 | `tests/integration/test_files_browse.py` |
| IT-INST-001 | `tests/integration/test_install_mode.py`slow |
| IT-SRV-003 | `tests/integration/test_notify_settings.py` |
| E2E-TERM/FILE/SCR/CRED/RET/SET/EXEC-002 | `frontend/e2e/pages/*.spec.mjs` |
## E2E
未在本机跑 Playwright(需 `NEXUS_E2E_PASSWORD` + API :8600)。CI main job 在配置密钥后可跑全量 `e2e/`
## 结论
后端缺口用例已落地并通过本地验证;矩阵已更新。安装向导 E2E 与 CH-EXEC 轮询终态仍标待写。
+101
View File
@@ -0,0 +1,101 @@
# Agent MCP 浏览器点页规程(Playwright MCP
> 用例编号见 `test-case-matrix.md` 中 `MCP-*` 行。
> 工具:Cursor **Playwright MCP**`browser_navigate`, `browser_snapshot`, `browser_click`, `browser_type` 等)。
## 0. 环境与安全
```text
BASE=http://127.0.0.1:8600/app/ # 本地优先
USER=admin # 或 NEXUS_E2E_USER
PASS=<来自 .env,勿写入报告>
```
| 规则 | 说明 |
|------|------|
| 禁止 | 生产批量推送、批量删机、真实 WebSSH 长连接 |
| 允许 | 只读浏览、打开对话框后取消、对 `test-*` 子机 dry-run |
| 证据 | 每用例结束写 `docs/reports/mcp-runs/YYYY-MM-DD-<MCP-ID>.md` |
报告模板:
```markdown
# MCP-PUSH-001 2026-06-08
- BASE: http://127.0.0.1:8600/app/
- 结果: PASS|FAIL
- 步骤: 1…n
- 截图: (可选路径)
- 沉淀: 是否需新增 E2E-PUSH-002
```
## 1. MCP-AUTH-001 — 登录页
1. `browser_navigate``{BASE}#/login`
2. `browser_snapshot` → 断言存在「用户名」「密码」输入与登录按钮
3. `browser_type` 用户名/密码 → `browser_click` 登录
4. `browser_snapshot` → URL 含 `#/` 且侧栏可见
**失败处理**401 检查 API `:8600/health``start-dev.sh`
## 2. MCP-DASH-001 — 仪表盘
1. 已登录状态下 `browser_navigate``#/`
2. `browser_snapshot` → 在线/离线/告警类统计文案存在
3. 记录数字是否为 `—` 或 0(无数据也算 PASS
## 3. MCP-PUSH-001 — 推送提交(非破坏性)
1. `browser_navigate``#/push`
2. `browser_snapshot` → 目标路径、文件选择、提交按钮
3. **仅当选中子机 hostname 含 `test` 时**继续;否则标记 SKIP 并注明原因
4. `browser_click` 提交 → 等待 ≤5s
5. `browser_snapshot`
- 提交按钮**不应**持续 disabled/loading >10s(对应 E2E-PUSH-002
- 进度区域或 toast 出现
6. 写报告;若 PASS7 日内补 `frontend/e2e/flows/push-submit.spec.mjs`
## 4. MCP-SRV-002 — 无 CSV 导出
1. `#/servers``browser_snapshot`
2. 全文搜索「导出」「CSV」→ 不得出现可点击导出按钮
3. 对应 E2E-SRV-002
## 5. MCP-ALT-001 / MCP-AUD-001 — 北京时间
1. 分别打开 `#/alerts``#/audit`
2. `browser_snapshot` 表格时间列
3. 期望:`YYYY-MM-DD HH:mm` 或带 `CST`/`北京时间` 语义;**不应**出现未格式化的 ISO `T...Z` 裸串
4.`UT-TIME-001` 一致
## 6. MCP-SCH-001 — 调度周期文案
1. `#/schedules` → 新建调度
2. `browser_snapshot` 周期/开始执行相关 label
3.`docs/changelog/2026-06-08-schedule-form-start-cycle-only.md` 一致
## 7. MCP-EXEC-001 — 执行看板
1. `#/executions`
2. 若有行,`browser_click` 展开首行
3. `browser_snapshot` → 详情面板无 500 错误 snackbar
## 8. 标准回归包(部署后 Agent 必跑)
按序执行,总时长目标 <15min:
```
MCP-AUTH-001 → MCP-DASH-001 → MCP-SRV-002 → MCP-PUSH-001(SKIP ok)
→ MCP-ALT-001 → MCP-AUD-001 → MCP-CMD-001
```
汇总写入 `docs/reports/YYYY-MM-DD-mcp-regression.md`
## 9. 与 Playwright CI 关系
| MCP | 沉淀为 E2E |
|-----|------------|
| MCP-PUSH-001 | E2E-PUSH-002 |
| MCP-SRV-002 | E2E-SRV-002 |
| MCP-ALT-001 | 扩 E2E-ALT-001 断言时间格式 |
MCP **不替代** CI;仅用于部署后探索与用例发现。
+257
View File
@@ -0,0 +1,257 @@
# Nexus 全站测试用例矩阵(SSOT)
> 编号规则:`<层>-<域>-<序号>`
> 层:`UT` 单元 · `IT` 集成 · `CH` 链条 · `E2E` Playwright · `MCP` Agent 浏览器
> 状态:`✅` 已有 · `🔲` 待写 · `🟡` 部分覆盖 · `⏭` 不验收(已确认豁免)
最后更新:2026-06-09
---
## 0. 横切能力
| ID | 层 | 用例 | 前置 | 步骤 | 期望 | 状态 | 实现位置 |
|----|-----|------|------|------|------|------|----------|
| UT-AUTH-001 | UT | JWT access 过期边界 | 固定 clock | 签发 exp-1s token | 401 + 中文提示 | ✅ | test_login_access.py |
| UT-AUTH-002 | UT | refresh 复用检测 | 双 refresh 同 jti | 第二次 refresh | 401 revoke 全家 | ✅ | test_auth_refresh_reuse.py |
| UT-VAL-001 | UT | 422 loc 中文化 | — | per_page=999 | detail 含 `每页条数` | ✅ | test_validation_errors_zh.py |
| UT-TIME-001 | UT | 北京时间格式化 | UTC 输入 | format_display_time | `Asia/Shanghai` 字符串 | ✅ | test_display_time.py |
| UT-SEC-001 | UT | 路径穿越拒绝 | — | `../etc/passwd` | ValidationError | ✅ | test_remote_path_validation.py |
| IT-AUTH-001 | IT | 登录换 token | API 活 | POST /api/auth/login | access+refresh | ✅ | test_api.py |
| IT-AUTH-002 | IT | 无 token 403 | — | GET /api/servers | 401/403 | ✅ | test_api.py |
| CH-AUTH-001 | CH | 登录→刷新→访问 | 活 API | login→refresh→GET servers | 200 | ✅ | tests/chain/test_auth_flow.py |
| E2E-AUTH-001 | E2E | 错误密码 | 浏览器 | 填错密码登录 | 页内错误提示 | ✅ | e2e/pages/auth.spec.mjs |
| MCP-AUTH-001 | MCP | 登录页渲染 | base URL | snapshot #/login | 表单+品牌可见 | ✅ | e2e/pages/login.spec.mjs |
---
## 1. 安装向导 `/app/install.html`
| ID | 层 | 用例 | 期望 | 状态 | 备注 |
|----|-----|------|------|------|------|
| UT-INST-001 | UT | 已安装锁 404 | /api/install/* 404 | ✅ | test_install_locked_404.py |
| IT-INST-001 | IT | 无 .env 仅 install 路由 | /health 与 /api/servers 行为 | ✅ | tests/integration/test_install_mode.py |
| E2E-INST-001 | E2E | 安装页可打开 | install.html 200 | ✅ | e2e/pages/install.spec.mjs |
| MCP-INST-001 | MCP | 安装步骤 UI | 各 step 控件可见 | ⏭ | **不验收**:生产已锁安装向导;E2E-INST-001 可达性已够 |
---
## 2. 仪表盘 Dashboard `#/`
| ID | 层 | 用例 | 期望 | 状态 | 实现 |
|----|-----|------|------|------|------|
| IT-DASH-001 | IT | GET /api/dashboard/stats | 200 + 数字字段 | ✅ | tests/integration/test_servers_dashboard.py |
| E2E-DASH-001 | E2E | 仪表盘加载 | 卡片/图表区域可见 | ✅ | full-acceptance #2 + dashboard-stats.spec.mjs |
| MCP-DASH-001 | MCP | 统计数字非空 | 在线/告警计数 | ✅ | 沉淀 E2E-DASH-001 |
---
## 3. 服务器 Servers `#/servers`
| ID | 层 | 用例 | 期望 | 状态 | 实现 |
|----|-----|------|------|------|------|
| UT-SRV-001 | UT | 列表排序键合法 | sort 白名单 | ✅ | test_server_list_sort.py |
| UT-SRV-002 | UT | status 字段枚举 | online/offline/… | ✅ | test_server_status_field.py |
| UT-SRV-003 | UT | target_path 过滤 | 非法路径拒绝 | ✅ | test_server_target_path_filter.py |
| IT-SRV-001 | IT | GET /api/servers 分页 | total+items | ✅ | test_api.py |
| IT-SRV-002 | IT | per_page>200 → 422 中文 | loc_zh | ✅ | 422 改造后 |
| IT-SRV-003 | IT | notify 开关 settings API | 审计+CUD | ✅ | tests/integration/test_notify_settings.py |
| CH-SRV-001 | CH | 列表→详情抽屉→保存 | DB 与响应一致 | ✅ | tests/chain/test_server_update_flow.py |
| E2E-SRV-001 | E2E | 表格+筛选 | 表头+搜索框 | ✅ | full-acceptance #3 |
| E2E-SRV-002 | E2E | **无 CSV 导出按钮** | 按钮不存在 | ✅ | e2e/pages/servers.spec.mjs |
| E2E-SRV-003 | E2E | 分类筛选可见 | chip/文案 | ✅ | e2e/pages/servers-filter.spec.mjs |
| MCP-SRV-001 | MCP | 打开筛选抽屉 | 分类/状态 chip | ✅ | 沉淀 E2E-SRV-003 |
---
## 4. 终端 Terminal `#/terminal`
| ID | 层 | 用例 | 期望 | 状态 | 实现 |
|----|-----|------|------|------|------|
| UT-TERM-001 | UT | WebSSH payload 校验 | 非法 host 拒绝 | ✅ | test_webssh_terminal_payload.py |
| IT-TERM-001 | IT | WS /ws/webssh 握手 | 401 无 token | ✅ | tests/integration/test_webssh_auth.py |
| E2E-TERM-001 | E2E | 页加载 idle 提示 | 未选服务器文案 | ✅ | e2e/pages/terminal.spec.mjs |
| MCP-TERM-001 | MCP | 选子机不连真 SSH | UI 状态 idle | ✅ | 沉淀 e2e/pages/terminal.spec.mjs |
---
## 5. 文件 Files `#/files`
| ID | 层 | 用例 | 期望 | 状态 | 实现 |
|----|-----|------|------|------|------|
| UT-FILE-001 | UT | ls 解析 | unix ls 输出 | ✅ | test_unix_ls.py |
| UT-FILE-002 | UT | browse etag | 304 条件 | ✅ | test_files_browse_etag.py |
| UT-FILE-003 | UT | 提权路径策略 | sudo 边界 | ✅ | test_files_elevation.py |
| IT-FILE-001 | IT | GET browse | 列表 JSON | ✅ | tests/integration/test_files_browse.py |
| CH-FILE-001 | CH | 浏览→上传 staging→确认 | staging 清理 | ✅ | tests/chain/test_file_staging_flow.py |
| E2E-FILE-001 | E2E | 文件页表格 | 路径栏可见 | ✅ | e2e/pages/files.spec.mjs |
| MCP-FILE-001 | MCP | 目录树展开一级 | 无 500 toast | ✅ | 沉淀 e2e/pages/files.spec.mjs |
---
## 6. 推送 Push `#/push`
| ID | 层 | 用例 | 期望 | 状态 | 实现 |
|----|-----|------|------|------|------|
| UT-PUSH-001 | UT | batch_id 生成唯一 | UUID 格式 | ✅ | test_sync_batch_id.py |
| UT-PUSH-002 | UT | rsync 权限位 | 映射正确 | ✅ | test_rsync_push_permissions.py |
| UT-PUSH-003 | UT | upload form 字段 | schema | ✅ | test_sync_upload_form.py |
| IT-PUSH-001 | IT | POST sync 立即 accepted | `accepted:true` 非阻塞 | ✅ | tests/integration/test_push_sync.py |
| IT-PUSH-002 | IT | GET /api/sync/logs | 分页 | ✅ | tests/integration/test_servers_dashboard.py |
| CH-PUSH-001 | CH | accepted→WS progress→log 行 | batch 状态终态 | ✅ | tests/chain/test_push_async_flow.py |
| E2E-PUSH-001 | E2E | 推送页控件 | 目标路径+提交 | ✅ | full-acceptance #6 |
| E2E-PUSH-002 | E2E | 提交后短暂 loading 非一直转圈 | submitting 结束 | ✅ | e2e/pages/push.spec.mjs |
| MCP-PUSH-001 | MCP | 选测试子机 dry-run 提交 | toast + 进度表 | ✅ | e2e/pages/push.spec.mjsmock 子机+API |
---
## 7. 脚本 Scripts `#/scripts`
| ID | 层 | 用例 | 期望 | 状态 | 实现 |
|----|-----|------|------|------|------|
| UT-SCR-001 | UT | dispatch 路由 | server_ids 校验 | ✅ | test_script_dispatch.py |
| IT-SCR-001 | IT | CRUD /api/scripts | 201/200/404 | ✅ | tests/integration/test_scripts_executions.py |
| E2E-SCR-001 | E2E | 脚本列表+新建按钮 | 无 JS 错误 | ✅ | e2e/pages/scripts.spec.mjs |
| MCP-SCR-001 | MCP | Monaco 编辑器加载 | 无 JS 错误 | ✅ | 沉淀 e2e/pages/scripts.spec.mjs |
---
## 8. 执行看板 Executions `#/executions`
| ID | 层 | 用例 | 期望 | 状态 | 实现 |
|----|-----|------|------|------|------|
| UT-EXEC-001 | UT | 进度分组 | 按 server 聚合 | ✅ | test_exec_detail_grouping.py |
| UT-EXEC-002 | UT | MySQL 持久化字段 | 状态机 | ✅ | test_script_execution_mysql_persist.py |
| IT-EXEC-001 | IT | GET /api/script-executions | 分页+filter | ✅ | tests/integration/test_scripts_executions.py |
| CH-EXEC-001 | CH | 创建执行→轮询→完成 | status=completed | ✅ | tests/chain/test_script_exec_flow.py |
| E2E-EXEC-001 | E2E | 看板页加载 | 表+筛选 | ✅ | e2e/pages/executions.spec.mjs |
| E2E-EXEC-002 | E2E | 展开执行详情行 | stdout/面板 | ✅ | e2e/pages/executions-detail.spec.mjs |
| MCP-EXEC-001 | MCP | 展开执行详情行 | stdout 片段 | ✅ | 沉淀 e2e/pages/executions-detail.spec.mjs |
---
## 9. 凭据 Credentials `#/credentials`
| ID | 层 | 用例 | 期望 | 状态 | 实现 |
|----|-----|------|------|------|------|
| UT-CRED-001 | UT | poller 周期 | mock clock | ✅ | test_credential_poller.py |
| IT-CRED-001 | IT | 列表不返回明文 secret | 脱敏 | ✅ | tests/integration/test_presets.py |
| E2E-CRED-001 | E2E | 凭据对话框表单校验 | 必填红框 | ✅ | e2e/pages/credentials.spec.mjs |
| MCP-CRED-001 | MCP | 新建凭据表单校验 | 必填红框 | ✅ | 沉淀 e2e/pages/credentials.spec.mjs |
---
## 10. 调度 Schedules `#/schedules`
| ID | 层 | 用例 | 期望 | 状态 | 实现 |
|----|-----|------|------|------|------|
| UT-SCH-001 | UT | next_run 计算 | cron/周期 | ✅ | test_schedule_next_run.py |
| UT-SCH-002 | UT | cycle 标签 | 仅开始周期 | ✅ | test_schedule_cycle.py |
| UT-SCH-003 | UT | runner once | 不重复触发 | ✅ | test_schedule_runner_once.py |
| IT-SCH-001 | IT | CRUD schedules | 审计 | ✅ | tests/integration/test_schedules.py |
| E2E-SCH-001 | E2E | 新建调度对话框 | ✅ | full-acceptance #9 |
| E2E-SCH-002 | E2E | 新建调度周期中文 | 开始执行周期 | ✅ | e2e/pages/schedules-cycle.spec.mjs |
| MCP-SCH-001 | MCP | 周期字段中文标签 | 与后端一致 | ✅ | 沉淀 E2E-SCH-002 |
---
## 11. 重试 Retries `#/retries`
| ID | 层 | 用例 | 期望 | 状态 | 实现 |
|----|-----|------|------|------|------|
| UT-RET-001 | UT | runner outcome | 成功/失败计数 | ✅ | test_retry_runner_outcome.py |
| IT-RET-001 | IT | GET /api/retries | 队列列表 | ✅ | tests/integration/test_retries_commands.py |
| E2E-RET-001 | E2E | 重试页无行级重试按钮(空表) | 表+筛选 | ✅ | e2e/pages/retries.spec.mjs |
| MCP-RET-001 | MCP | 手动重试按钮 disabled 态 | 无选中时 | ✅ | 沉淀 e2e/pages/retries.spec.mjs |
---
## 12. 命令日志 Commands `#/commands`
| ID | 层 | 用例 | 期望 | 状态 | 实现 |
|----|-----|------|------|------|------|
| IT-CMD-001 | IT | 命令/推送/会话三视图 API | 200 | ✅ | tests/integration/test_retries_commands.py |
| E2E-CMD-001 | E2E | 切换三视图 | ✅ | full-acceptance #11 |
| E2E-CMD-002 | E2E | 推送日志时间格式 | 无裸 ISO UTC | ✅ | e2e/pages/commands-time.spec.mjs |
| MCP-CMD-001 | MCP | 推送日志表北京时间 | 列格式 | ✅ | 沉淀 E2E-CMD-002 |
---
## 13. 告警 Alerts `#/alerts`
| ID | 层 | 用例 | 期望 | 状态 | 实现 |
|----|-----|------|------|------|------|
| UT-ALT-001 | UT | Agent 恢复去重 | 同 metric 1 条 | ✅ | test_agent_recovery_dedup.py |
| UT-ALT-002 | UT | Telegram 时间格式 | 北京时间 | ✅ | test_telegram_time_format.py |
| IT-ALT-001 | IT | GET /api/alerts + WS | 推送事件 | ✅ | tests/integration/test_alerts_audit.py |
| CH-ALT-001 | CH | 心跳告警→恢复→TG 一条 | Redis+mock TG | ✅ | tests/chain/test_alert_recovery_flow.py |
| E2E-ALT-001 | E2E | 告警筛选 UI | ✅ | full-acceptance #12 |
| E2E-ALT-002 | E2E | 告警表时间格式 | 无裸 ISO UTC | ✅ | e2e/pages/alerts-time.spec.mjs |
| MCP-ALT-001 | MCP | 告警表时间列 | 无 UTC 裸字符串 | ✅ | 沉淀 E2E-ALT-002 |
---
## 14. 审计 Audit `#/audit`
| ID | 层 | 用例 | 期望 | 状态 | 实现 |
|----|-----|------|------|------|------|
| IT-AUD-001 | IT | GET /api/audit 分页 | CUD 记录 | ✅ | tests/integration/test_alerts_audit.py |
| E2E-AUD-001 | E2E | 审计页+分页 | ✅ | full-acceptance #13 |
| E2E-AUD-002 | E2E | 审计表时间格式 | 无裸 ISO UTC | ✅ | e2e/pages/audit-time.spec.mjs |
| MCP-AUD-001 | MCP | 时间列北京时间 | 与设置无关 | ✅ | 沉淀 E2E-AUD-002 |
---
## 15. 设置 Settings `#/settings`
| ID | 层 | 用例 | 期望 | 状态 | 实现 |
|----|-----|------|------|------|------|
| UT-SET-001 | UT | Telegram 设置校验 | token 格式 | ✅ | test_settings_telegram.py |
| IT-SET-001 | IT | PATCH settings 禁止覆盖密钥 | 403 | ✅ | tests/integration/test_settings_security.py |
| E2E-SET-001 | E2E | 各 section 可见 | ✅ | e2e/pages/settings.spec.mjs + full-acceptance #14 |
| MCP-SET-001 | MCP | 保存设置 toast | 成功中文 | ✅ | e2e/pages/settings.spec.mjs E2E-SET-002 |
---
## 16. Agent 子机(无 SPA 页)
| ID | 层 | 用例 | 期望 | 状态 | 实现 |
|----|-----|------|------|------|------|
| UT-AGT-001 | UT | heartbeat TTL | Redis 过期 | ✅ | test_agent_heartbeat_ttl.py |
| UT-AGT-002 | UT | per-server API key | 隔离 | ✅ | test_agent_per_server_key.py |
| UT-AGT-003 | UT | version 上报 | 语义化版本 | ✅ | test_agent_version.py |
| IT-AGT-001 | IT | POST /api/agent/heartbeat | 200 | ✅ | tests/integration/test_agent_heartbeat.py |
| CH-AGT-001 | CH | 心跳→Redis→仪表盘计数 | 一致 | ✅ | tests/chain/test_agent_dashboard_flow.py |
---
## 17. 批量 / 后台任务
| ID | 层 | 用例 | 期望 | 状态 | 实现 |
|----|-----|------|------|------|------|
| UT-BAT-001 | UT | batch job 持久化 | ✅ | test_server_batch_job_persist.py |
| UT-BAT-002 | UT | reconcile 状态机 | ✅ | test_server_batch_reconcile.py |
| UT-SYNC-001 | UT | sync_log purge | ✅ | test_sync_log_purge.py |
| CH-BAT-001 | CH | 批量操作→job 表→完成 | ✅ | tests/chain/test_batch_job_flow.py |
---
## 覆盖率汇总(2026-06-08 估算)
| 层 | 已有 | 待写 | 合计 |
|----|------|------|------|
| UT | ~45 | ~15 | ~60 |
| IT | ~26 | ~11 | ~37 |
| CH | ~9 | ~5 | ~14 |
| E2E | ~35 | ~2 | ~37 |
| MCP | ~18(沉淀 E2E | ~3manual/安装) | ~21 |
| **合计** | **~133** | **~36** | **~169** |
---
## PR 检查清单
- [ ] 改动域在表中至少更新 1 行状态
- [ ] 新 API 必有 `IT-*`;新纯函数必有 `UT-*`
- [ ] UI 行为变更必有 `E2E-*``MCP-*` 计划
- [ ] 破坏性 E2E 仅对测试子机
+44
View File
@@ -0,0 +1,44 @@
/**
* Worker-scoped auth: one API login per worker; persist rotated refresh cookies between tests.
*/
import { test as base, request } from '@playwright/test'
const BASE = (process.env.NEXUS_E2E_BASE || 'https://api.synaglobal.vip').replace(/\/$/, '')
const USER = process.env.NEXUS_E2E_USER || 'admin'
const PASS = process.env.NEXUS_E2E_PASSWORD || ''
/** @type {import('@playwright/test').StorageState | null} */
let workerStorageState = null
async function ensureWorkerStorageState() {
if (workerStorageState) return workerStorageState
if (!PASS) throw new Error('Set NEXUS_E2E_PASSWORD for E2E')
const ctx = await request.newContext({ baseURL: BASE, ignoreHTTPSErrors: true })
try {
const res = await ctx.post('/api/auth/login', {
data: { username: USER, password: PASS },
})
if (!res.ok()) {
throw new Error(`Worker login failed (${res.status()}): ${await res.text()}`)
}
workerStorageState = await ctx.storageState()
return workerStorageState
} finally {
await ctx.dispose()
}
}
export const test = base.extend({
context: async ({ browser }, use) => {
const storageState = await ensureWorkerStorageState()
const context = await browser.newContext({ storageState })
try {
await use(context)
} finally {
workerStorageState = await context.storageState()
await context.close()
}
},
})
export { expect } from '@playwright/test'
+15 -34
View File
@@ -1,34 +1,12 @@
/**
* Full-site UI acceptance — every SPA route + primary controls (no destructive push).
* Run from frontend/: NEXUS_E2E_USER=admin NEXUS_E2E_PASSWORD=*** npx playwright test e2e/full-acceptance.spec.mjs
* Run from frontend/: NEXUS_E2E_USER=admin NEXUS_E2E_PASSWORD=*** npm run test:e2e
*/
import { test, expect } from '@playwright/test'
const BASE = process.env.NEXUS_E2E_BASE || 'https://api.synaglobal.vip'
const USER = process.env.NEXUS_E2E_USER || 'admin'
const PASS = process.env.NEXUS_E2E_PASSWORD || ''
const APP = `${BASE.replace(/\/$/, '')}/app/`
import { test, expect } from './fixtures.mjs'
import { APP, USER, login, nav } from './helpers.mjs'
test.describe.configure({ mode: 'serial' })
async function login(page) {
if (!PASS) throw new Error('Set NEXUS_E2E_PASSWORD')
await page.goto(`${APP}#/login`)
await page.getByLabel('用户名').fill(USER)
await page.getByLabel('密码', { exact: true }).fill(PASS)
const totp = page.getByLabel('TOTP 验证码')
if (await totp.isVisible().catch(() => false)) {
throw new Error('TOTP required — disable TOTP on test admin or provide code')
}
await page.getByRole('button', { name: '登录' }).click()
await expect(page).not.toHaveURL(/#\/login/, { timeout: 20000 })
}
async function nav(page, hash) {
await page.goto(`${APP}#${hash}`)
await page.waitForLoadState('domcontentloaded')
}
test('01 login + dashboard stats', async ({ page }) => {
await login(page)
await nav(page, '/')
@@ -38,7 +16,7 @@ test('01 login + dashboard stats', async ({ page }) => {
test('02 servers: list, batch select, add dialog', async ({ page }) => {
await login(page)
await nav(page, '/servers')
await page.getByRole('button', { name: '添加' }).click()
await page.getByRole('button', { name: '添加', exact: true }).click()
await expect(page.getByText('添加服务器')).toBeVisible()
await page.getByRole('button', { name: '取消' }).click()
const cbs = page.locator('table tbody tr input[type="checkbox"]')
@@ -49,12 +27,11 @@ test('02 servers: list, batch select, add dialog', async ({ page }) => {
}
})
test('03 global search', async ({ page }) => {
test('03 dashboard nav drawer', async ({ page }) => {
await login(page)
await nav(page, '/')
const search = page.getByPlaceholder('搜索服务器、脚本、凭据...')
await search.fill('a')
await page.waitForTimeout(400)
await expect(page.locator('.v-card').first()).toBeVisible({ timeout: 20000 })
await expect(page.getByRole('link', { name: '服务器' })).toBeVisible()
})
test('04 terminal page', async ({ page }) => {
@@ -86,12 +63,14 @@ test('07 scripts: new dialog cancel', async ({ page }) => {
test('08 credentials: tabs', async ({ page }) => {
await login(page)
await nav(page, '/credentials')
await expect(page.getByText('凭据管理')).toBeVisible({ timeout: 20000 })
await nav(page, '/servers')
await page.getByRole('button', { name: '凭据' }).click()
const dialog = page.getByRole('dialog')
await expect(dialog.getByText('凭据管理')).toBeVisible({ timeout: 20000 })
await page.getByRole('button', { name: 'SSH 密钥' }).click()
await page.getByRole('button', { name: '数据库凭据' }).click()
await page.getByRole('button', { name: '密码预设' }).click()
await expect(page.getByRole('button', { name: '添加' })).toBeVisible()
await expect(page.getByRole('dialog').getByRole('button', { name: '添加', exact: true })).toBeVisible()
})
test('09 schedules: new dialog cancel', async ({ page }) => {
@@ -111,7 +90,9 @@ test('10 retries list', async ({ page }) => {
test('11 commands: command, session and push log views', async ({ page }) => {
await login(page)
await nav(page, '/commands')
await expect(page.getByText('命令日志')).toBeVisible({ timeout: 20000 })
await expect(page.locator('main .v-card-title').filter({ hasText: '命令日志' })).toBeVisible({
timeout: 20000,
})
await page.locator('.v-card-title .v-select').first().click()
await page.locator('.v-overlay-container').getByText('推送日志').click()
await expect(page.locator('table').first()).toBeVisible()
+60
View File
@@ -0,0 +1,60 @@
/**
* Shared Playwright helpers for Nexus SPA E2E.
*/
import { expect } from '@playwright/test'
export const BASE = process.env.NEXUS_E2E_BASE || 'https://api.synaglobal.vip'
export const USER = process.env.NEXUS_E2E_USER || 'admin'
export const PASS = process.env.NEXUS_E2E_PASSWORD || ''
export const APP = `${BASE.replace(/\/$/, '')}/app/`
const SIDEBAR_LINKS = {
'/': '仪表盘',
'/servers': '服务器',
'/terminal': '终端',
'/files': '文件管理',
'/push': '推送',
'/scripts': '脚本库',
'/executions': '执行记录',
'/schedules': '调度',
'/retries': '重试队列',
'/commands': '命令日志',
'/alerts': '告警中心',
'/audit': '审计日志',
}
/** Restore session from worker refresh cookie (avoids parallel UI logins / lockout). */
export async function login(page) {
await page.goto(`${APP}#/`, { waitUntil: 'domcontentloaded' })
await expect(page).not.toHaveURL(/#\/login/, { timeout: 20000 })
await expect(page.getByRole('button', { name: 'admin' })).toBeVisible({ timeout: 15000 })
}
/** Navigate via drawer clicks (reliable with Vue keep-alive; hash goto alone is flaky). */
export async function nav(page, hash) {
const path = hash.startsWith('/') ? hash : `/${hash}`
const routePath = path.split('?')[0]
const escaped = routePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
if (routePath === '/settings') {
await page.locator('.v-navigation-drawer').getByText('设置', { exact: true }).click()
} else {
const label = SIDEBAR_LINKS[routePath]
if (!label) throw new Error(`Unknown route for nav(): ${path}`)
await page.getByRole('link', { name: label, exact: true }).click()
}
await expect(page).toHaveURL(new RegExp(`#${escaped}(?:\\?|$)`), { timeout: 20000 })
if (path.includes('?')) {
await page.evaluate((p) => {
window.location.hash = `#${p}`
}, path)
await expect(page).toHaveURL(new RegExp(`#${escaped}\\?`), { timeout: 10000 })
}
await page.waitForLoadState('networkidle').catch(() => {})
}
/** ISO UTC bare string e.g. 2026-06-08T12:00:00Z — should not appear in display columns */
export const ISO_UTC_RE = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/
+18
View File
@@ -0,0 +1,18 @@
/** E2E-ALT-002 — alert table times not raw ISO UTC (MCP-ALT-001沉淀) */
import { test, expect } from '../fixtures.mjs'
import { ISO_UTC_RE, login, nav } from '../helpers.mjs'
test('E2E-ALT-002 alert times not bare ISO UTC', async ({ page }) => {
await login(page)
await nav(page, '/alerts')
await expect(page.getByText(/告警/).first()).toBeVisible({ timeout: 20000 })
const cells = page.locator('table tbody td')
const count = await cells.count()
for (let i = 0; i < Math.min(count, 20); i++) {
const text = await cells.nth(i).innerText()
if (/\d{4}-\d{2}-\d{2}/.test(text)) {
expect(text).not.toMatch(ISO_UTC_RE)
}
}
})
+18
View File
@@ -0,0 +1,18 @@
/** E2E-AUD-002 — audit table Beijing time display (MCP-AUD-001沉淀) */
import { test, expect } from '../fixtures.mjs'
import { ISO_UTC_RE, login, nav } from '../helpers.mjs'
test('E2E-AUD-002 audit times not bare ISO UTC', async ({ page }) => {
await login(page)
await nav(page, '/audit')
await expect(page.getByText(/审计/).first()).toBeVisible({ timeout: 20000 })
const cells = page.locator('table tbody td')
const count = await cells.count()
for (let i = 0; i < Math.min(count, 20); i++) {
const text = await cells.nth(i).innerText()
if (/\d{4}-\d{2}-\d{2}/.test(text)) {
expect(text).not.toMatch(ISO_UTC_RE)
}
}
})
+14
View File
@@ -0,0 +1,14 @@
/** E2E-AUTH-001 — wrong password shows error, stays on login */
import { test, expect } from '@playwright/test'
import { APP, USER } from '../helpers.mjs'
test.use({ storageState: { cookies: [], origins: [] } })
test('E2E-AUTH-001 wrong password shows error', async ({ page }) => {
await page.goto(`${APP}#/login`)
await page.getByLabel('用户名').fill(USER)
await page.getByLabel('密码', { exact: true }).fill('definitely-wrong-password-e2e')
await page.getByRole('button', { name: '登录' }).click()
await expect(page).toHaveURL(/#\/login/, { timeout: 15000 })
await expect(page.getByText(/错误|失败|无效|不正确/).first()).toBeVisible({ timeout: 10000 })
})
+24
View File
@@ -0,0 +1,24 @@
/** E2E-CMD-002 — push log view time columns (MCP-CMD-001沉淀) */
import { test, expect } from '../fixtures.mjs'
import { ISO_UTC_RE, login, nav } from '../helpers.mjs'
test('E2E-CMD-002 push log times not bare ISO UTC', async ({ page }) => {
await login(page)
await nav(page, '/commands')
await expect(page.locator('main .v-card-title').filter({ hasText: '命令日志' })).toBeVisible({
timeout: 20000,
})
await page.locator('.v-card-title .v-select').first().click()
await page.locator('.v-overlay-container').getByText('推送日志').click()
await expect(page.locator('table').first()).toBeVisible()
const cells = page.locator('table tbody td')
const count = await cells.count()
for (let i = 0; i < Math.min(count, 20); i++) {
const text = await cells.nth(i).innerText()
if (/\d{4}-\d{2}-\d{2}/.test(text)) {
expect(text).not.toMatch(ISO_UTC_RE)
}
}
})
+15
View File
@@ -0,0 +1,15 @@
/** E2E-CRED-001 — credentials dialog required fields (MCP-CRED-001沉淀) */
import { test, expect } from '../fixtures.mjs'
import { login, nav } from '../helpers.mjs'
test('E2E-CRED-001 password preset form validation', async ({ page }) => {
await login(page)
await nav(page, '/servers')
await page.getByRole('button', { name: '凭据' }).click()
const dialog = page.getByRole('dialog')
await expect(dialog.getByText('凭据管理')).toBeVisible({ timeout: 20000 })
await dialog.getByRole('button', { name: '添加', exact: true }).click()
await expect(dialog.getByText('新建密码预设')).toBeVisible()
await dialog.getByRole('button', { name: '保存' }).click()
await expect(page.getByText('请填写预设名称')).toBeVisible({ timeout: 5000 })
})
@@ -0,0 +1,10 @@
/** E2E-DASH-001 extended — dashboard stat cards visible (MCP-DASH-001沉淀) */
import { test, expect } from '../fixtures.mjs'
import { login, nav } from '../helpers.mjs'
test('E2E-DASH-001 dashboard cards visible', async ({ page }) => {
await login(page)
await nav(page, '/')
await expect(page.locator('.v-card').first()).toBeVisible({ timeout: 20000 })
expect(await page.locator('.v-card').count()).toBeGreaterThan(0)
})
@@ -0,0 +1,22 @@
/** E2E-EXEC-002 — execution row expand when data exists (MCP-EXEC-001沉淀) */
import { test, expect } from '../fixtures.mjs'
import { login, nav } from '../helpers.mjs'
test('E2E-EXEC-002 execution detail expand', async ({ page }) => {
await login(page)
await nav(page, '/executions')
await expect(page.getByRole('combobox', { name: '状态筛选' })).toBeVisible({ timeout: 20000 })
const rows = page.locator('tbody tr')
const count = await rows.count()
if (count === 0) {
await expect(page.getByText('暂无数据')).toBeVisible()
return
}
const expand = rows.first().locator('button').first()
if (await expand.isVisible()) {
await expand.click()
await expect(page.locator('.v-expansion-panel-text, pre, code').first()).toBeVisible({
timeout: 10000,
})
}
})
+11
View File
@@ -0,0 +1,11 @@
/** E2E-EXEC-001 — executions board loads with filters */
import { test, expect } from '../fixtures.mjs'
import { login, nav } from '../helpers.mjs'
test('E2E-EXEC-001 executions page table and filters', async ({ page }) => {
await login(page)
await nav(page, '/executions')
await expect(page.getByRole('combobox', { name: '类型' })).toBeVisible({ timeout: 20000 })
await expect(page.getByRole('combobox', { name: '状态筛选' })).toBeVisible()
await expect(page.locator('table').first()).toBeVisible()
})
+12
View File
@@ -0,0 +1,12 @@
/** E2E-FILE-001 — files page loads toolbar (MCP-FILE-001沉淀) */
import { test, expect } from '../fixtures.mjs'
import { login, nav } from '../helpers.mjs'
test('E2E-FILE-001 files page server picker visible', async ({ page }) => {
const errors = []
page.on('pageerror', (e) => errors.push(e.message))
await login(page)
await nav(page, '/files')
await expect(page.getByLabel('选择服务器')).toBeVisible({ timeout: 20000 })
expect(errors).toEqual([])
})
+10
View File
@@ -0,0 +1,10 @@
/** E2E-INST-001 — install.html reachable (MCP-INST-001 step UI waived per product) */
import { test, expect } from '@playwright/test'
const BASE = process.env.NEXUS_E2E_BASE || 'http://127.0.0.1:8600'
test('E2E-INST-001 install.html reachable', async ({ page }) => {
const res = await page.goto(`${BASE.replace(/\/$/, '')}/app/install.html`)
expect(res?.status()).toBe(200)
await expect(page.getByText('Nexus 6.0 安装向导')).toBeVisible({ timeout: 15000 })
})
+12
View File
@@ -0,0 +1,12 @@
/** E2E-AUTH-002 / MCP-AUTH-001 — login page renders without submitting */
import { test, expect } from '@playwright/test'
import { APP } from '../helpers.mjs'
test.use({ storageState: { cookies: [], origins: [] } })
test('MCP-AUTH-001 login form visible', async ({ page }) => {
await page.goto(`${APP}#/login`)
await expect(page.getByLabel('用户名')).toBeVisible()
await expect(page.getByLabel('密码', { exact: true })).toBeVisible()
await expect(page.getByRole('button', { name: '登录' })).toBeVisible()
})
+79
View File
@@ -0,0 +1,79 @@
/** E2E-PUSH-002 — push submit ends submitting state quickly */
import { test, expect } from '../fixtures.mjs'
import { login, nav } from '../helpers.mjs'
const mockServer = {
id: 9001,
name: 'e2e-push-mock',
domain: '10.0.0.1',
target_path: '/var/www',
category: null,
is_online: true,
status: 'online',
}
test('E2E-PUSH-002 submitting clears after accepted response', async ({ page }) => {
await page.route('**/api/servers**', async (route) => {
if (route.request().method() !== 'GET') {
await route.continue()
return
}
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
items: [mockServer],
total: 1,
page: 1,
per_page: 200,
pages: 1,
}),
})
})
await page.route('**/api/sync/validate-source-path', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
valid: true,
path: '/tmp/nexus-e2e-src',
file_count: 1,
size_bytes: 100,
}),
})
})
await page.route('**/api/sync/files**', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
accepted: true,
batch_id: 'aabbccddeeff',
total: 1,
completed: 0,
failed: 0,
cancelled: 0,
results: {},
}),
})
})
await login(page)
await nav(page, '/push')
await page.getByPlaceholder('/path/to/files').fill('/tmp/nexus-e2e-src')
await page.getByRole('button', { name: '验证路径' }).click()
await expect(page.getByText('e2e-push-mock')).toBeVisible({ timeout: 20000 })
await page.getByText('e2e-push-mock', { exact: true }).click()
const pushBtn = page.getByRole('button', { name: '推送', exact: true })
const postDone = page.waitForResponse(
(r) => r.request().method() === 'POST' && r.url().includes('/sync/files'),
{ timeout: 20000 },
)
await pushBtn.click()
const res = await postDone
expect(res.status()).toBe(200)
const body = await res.json()
expect(body.accepted).toBe(true)
})
+11
View File
@@ -0,0 +1,11 @@
/** E2E-RET-001 — retries queue page (MCP-RET-001沉淀) */
import { test, expect } from '../fixtures.mjs'
import { login, nav } from '../helpers.mjs'
test('E2E-RET-001 retries table without bulk retry actions', async ({ page }) => {
await login(page)
await nav(page, '/retries')
await expect(page.getByRole('combobox', { name: '状态筛选' })).toBeVisible({ timeout: 20000 })
const retryButtons = page.getByRole('button', { name: '重试', exact: true })
expect(await retryButtons.count()).toBe(0)
})
@@ -0,0 +1,12 @@
/** E2E-SCH-002 — schedule cycle Chinese labels (MCP-SCH-001沉淀) */
import { test, expect } from '../fixtures.mjs'
import { login, nav } from '../helpers.mjs'
test('E2E-SCH-002 new schedule shows 开始执行周期', async ({ page }) => {
await login(page)
await nav(page, '/schedules')
await page.getByRole('button', { name: '新建调度' }).click()
await expect(page.getByText('开始执行周期').first()).toBeVisible()
await expect(page.getByText('新建调度').first()).toBeVisible()
await page.getByRole('button', { name: '取消' }).click()
})
+12
View File
@@ -0,0 +1,12 @@
/** E2E-SCR-001 — scripts library loads (MCP-SCR-001沉淀) */
import { test, expect } from '../fixtures.mjs'
import { login, nav } from '../helpers.mjs'
test('E2E-SCR-001 scripts page toolbar without JS errors', async ({ page }) => {
const errors = []
page.on('pageerror', (e) => errors.push(e.message))
await login(page)
await nav(page, '/scripts')
await expect(page.getByRole('button', { name: '新建脚本' })).toBeVisible({ timeout: 20000 })
expect(errors).toEqual([])
})
@@ -0,0 +1,9 @@
/** E2E-SRV-003 — servers category filter chips (MCP-SRV-001沉淀) */
import { test, expect } from '../fixtures.mjs'
import { login, nav } from '../helpers.mjs'
test('E2E-SRV-003 category filter visible', async ({ page }) => {
await login(page)
await nav(page, '/servers')
await expect(page.getByText('分类筛选')).toBeVisible({ timeout: 20000 })
})
+9
View File
@@ -0,0 +1,9 @@
/** E2E-SRV-002 — servers page has no CSV export button */
import { test, expect } from '../fixtures.mjs'
import { login, nav } from '../helpers.mjs'
test('E2E-SRV-002 no CSV export button', async ({ page }) => {
await login(page)
await nav(page, '/servers')
await expect(page.getByRole('button', { name: /导出|CSV/i })).toHaveCount(0)
})
+37
View File
@@ -0,0 +1,37 @@
/** E2E-SET-001 / MCP-SET-001 — settings save control + success toast */
import { test, expect } from '../fixtures.mjs'
import { login, nav } from '../helpers.mjs'
test('E2E-SET-001 settings page save control', async ({ page }) => {
await login(page)
await nav(page, '/settings')
await expect(page.getByText('系统设置')).toBeVisible({ timeout: 20000 })
await expect(page.getByRole('button', { name: '保存设置' })).toBeVisible()
})
test('E2E-SET-002 settings save success toast', async ({ page }) => {
await login(page)
await page.route(/\/api\/settings\//, async (route) => {
if (route.request().method() === 'PUT') {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ key: 'system_name', value: 'Nexus' }),
})
return
}
await route.continue()
})
await nav(page, '/settings')
await expect(page.getByLabel('系统名称')).toBeVisible({ timeout: 20000 })
const title = page.getByLabel('系统标题')
if (!(await title.inputValue()).trim()) {
await title.fill('Nexus Ops E2E')
}
const saveReq = page.waitForRequest(
(req) => req.method() === 'PUT' && req.url().includes('/api/settings/'),
)
await page.getByRole('button', { name: '保存设置' }).click()
await saveReq
await expect(page.getByText('设置已保存')).toBeVisible({ timeout: 10000 })
})
+9
View File
@@ -0,0 +1,9 @@
/** E2E-TERM-001 — terminal idle state without live SSH (MCP-TERM-001沉淀) */
import { test, expect } from '../fixtures.mjs'
import { login, nav } from '../helpers.mjs'
test('E2E-TERM-001 terminal page idle prompt', async ({ page }) => {
await login(page)
await nav(page, '/terminal')
await expect(page.getByText('选择一台服务器开始会话')).toBeVisible({ timeout: 20000 })
})
+1 -1
View File
@@ -9,7 +9,7 @@
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build --force",
"test:e2e": "playwright test e2e/full-acceptance.spec.mjs"
"test:e2e": "playwright test e2e"
},
"dependencies": {
"@fontsource/roboto": "^5.2.10",
+18
View File
@@ -36,6 +36,11 @@
</v-chip>
</template>
<template #item.source_path="{ item }">
<span v-if="item.schedule_type === 'script'" class="text-caption">{{ scriptSourceLabel(item) }}</span>
<span v-else class="text-caption">{{ item.source_path || '—' }}</span>
</template>
<template #item.enabled="{ item }">
<v-switch
v-model="item.enabled"
@@ -327,6 +332,19 @@ function serverIdCount(raw: unknown): number {
return parseServerIds(raw).length
}
function scriptSourceLabel(item: ScheduleItem): string {
if (item.script_id) {
const script = scriptList.value.find((s) => s.id === item.script_id)
if (script) return script.name
return `脚本 #${item.script_id}`
}
const content = item.script_content?.trim()
if (content) {
return content.length > 48 ? `${content.slice(0, 48)}` : content
}
return '—'
}
function normalizeSchedule(row: ScheduleItem): ScheduleItem {
return { ...row, server_ids: parseServerIds(row.server_ids) }
}
+66 -4
View File
@@ -9,6 +9,7 @@ Nexus 全功能自动化验收 — 统一入口
python scripts/run_nexus_acceptance.py --base http://127.0.0.1:8600
python scripts/run_nexus_acceptance.py --with-ui
python scripts/run_nexus_acceptance.py --with-unit
python scripts/run_nexus_acceptance.py --with-chain
python scripts/run_nexus_acceptance.py --json reports/acceptance.json
环境变量(与 tests/test_api.py 相同):
@@ -282,11 +283,52 @@ def _run_pytest_unit(timeout: int = 300) -> tuple[list[CheckRecord], int]:
], proc.returncode
def _run_pytest_markers(marker_expr: str, feature_id: str, label: str, timeout: int = 300) -> tuple[list[CheckRecord], int]:
"""Run pytest with -m expression (integration / chain)."""
t0 = time.perf_counter()
proc = subprocess.run(
[
sys.executable,
"-m",
"pytest",
str(TESTS_DIR),
"-m",
marker_expr,
"-q",
"--tb=no",
],
cwd=str(REPO_ROOT),
capture_output=True,
text=True,
timeout=timeout,
encoding="utf-8",
errors="replace",
)
elapsed = (time.perf_counter() - t0) * 1000
out = (proc.stdout or "") + (proc.stderr or "")
m = re.search(r"(\d+)\s+passed", out)
passed_n = int(m.group(1)) if m else 0
m_fail = re.search(r"(\d+)\s+failed", out)
failed_n = int(m_fail.group(1)) if m_fail else 0
status = "pass" if proc.returncode == 0 else "fail"
layer = "chain" if "chain" in marker_expr else "integration"
return [
CheckRecord(
f"pytest {label} ({passed_n} passed, {failed_n} failed)",
status,
feature_id,
layer,
message=out.strip()[-500:] if status == "fail" else "",
duration_ms=elapsed,
)
], proc.returncode
def _run_playwright(env: dict[str, str], timeout: int = 900) -> tuple[list[CheckRecord], int]:
frontend = REPO_ROOT / "frontend"
spec = frontend / "e2e" / "full-acceptance.spec.mjs"
if not spec.is_file():
return [CheckRecord("playwright", "skip", "ui_dashboard", "ui", message="缺少 e2e spec")], 0
e2e_dir = frontend / "e2e"
if not e2e_dir.is_dir():
return [CheckRecord("playwright", "skip", "ui_dashboard", "ui", message="缺少 e2e 目录")], 0
if not (frontend / "node_modules").is_dir():
return [
CheckRecord(
@@ -314,7 +356,7 @@ def _run_playwright(env: dict[str, str], timeout: int = 900) -> tuple[list[Check
"npx",
"playwright",
"test",
"e2e/full-acceptance.spec.mjs",
"e2e",
"--reporter=line",
],
cwd=str(frontend),
@@ -427,6 +469,12 @@ def main() -> int:
parser.add_argument("--base", default=None, help="API 基址,默认 NEXUS_TEST_BASE")
parser.add_argument("--with-ui", action="store_true", help="运行 Playwright UI 验收(需 Node")
parser.add_argument("--with-unit", action="store_true", help="运行 pytest 单元测试子集")
parser.add_argument("--with-chain", action="store_true", help="运行 pytest 链条测试 (tests/chain)")
parser.add_argument(
"--with-integration",
action="store_true",
help="运行 pytest 集成测试 (tests/integration)",
)
parser.add_argument("--skip-api", action="store_true", help="跳过 test_api / test_full_site_api")
parser.add_argument("--json", dest="json_path", default=None, help="写入 JSON 报告路径")
args = parser.parse_args()
@@ -501,6 +549,20 @@ def main() -> int:
phases["unit_tests"] = {"exit_code": code}
exit_codes.append(code)
if args.with_integration:
print("\n>>> 阶段: integration_tests")
it_checks, code = _run_pytest_markers("integration", "integration_api", "integration")
all_checks.extend(it_checks)
phases["integration_tests"] = {"exit_code": code}
exit_codes.append(code)
if args.with_chain:
print("\n>>> 阶段: chain_tests")
ch_checks, code = _run_pytest_markers("chain", "chain_push", "chain")
all_checks.extend(ch_checks)
phases["chain_tests"] = {"exit_code": code}
exit_codes.append(code)
if args.with_ui:
print("\n>>> 阶段: playwright_ui")
ui_env = env.copy()
+76 -26
View File
@@ -226,58 +226,95 @@ async def delete_node(
# ── SSH Sessions & Command Logs ──
@router.get("/ssh-sessions", response_model=list)
@router.get("/ssh-sessions", response_model=dict)
async def list_ssh_sessions(
server_id: Optional[int] = Query(None, description="Filter by server ID"),
page: int = Query(1, ge=1),
per_page: int = Query(30, ge=1, le=200),
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),
admin: Admin = Depends(get_current_admin),
db: AsyncSession = Depends(get_db),
):
"""List SSH sessions with server name"""
from server.infrastructure.database.ssh_session_repo import SshSessionRepositoryImpl
from sqlalchemy import select
from server.domain.models import SshSession, Server
"""Paginated SSH sessions with server name."""
from sqlalchemy import func, select
from server.domain.models import Server, SshSession
repo = SshSessionRepositoryImpl(db)
if per_page:
limit = per_page
if page and per_page:
offset = (page - 1) * per_page
filters = []
if server_id:
sessions = await repo.get_by_server(server_id, limit)
else:
result = await db.execute(select(SshSession).order_by(SshSession.started_at.desc()).limit(limit))
sessions = list(result.scalars().all())
filters.append(SshSession.server_id == server_id)
count_q = select(func.count(SshSession.id))
data_q = select(SshSession).order_by(SshSession.started_at.desc())
if filters:
count_q = count_q.where(*filters)
data_q = data_q.where(*filters)
total = int((await db.execute(count_q)).scalar() or 0)
result = await db.execute(data_q.offset(offset).limit(limit))
sessions = list(result.scalars().all())
# Batch load server names
server_ids = {s.server_id for s in sessions if s.server_id}
server_names = {}
if server_ids:
srv_result = await db.execute(select(Server.id, Server.name).where(Server.id.in_(server_ids)))
server_names = dict(srv_result.all())
return [_ssh_session_to_dict(s, server_names.get(s.server_id)) for s in sessions]
import math
pages = math.ceil(total / limit) if limit and total > 0 else 1
return {
"items": [_ssh_session_to_dict(s, server_names.get(s.server_id)) for s in sessions],
"total": total,
"page": page,
"per_page": limit,
"pages": pages,
"limit": limit,
"offset": offset,
}
@router.get("/command-logs", response_model=list)
@router.get("/command-logs", response_model=dict)
async def list_command_logs(
server_id: Optional[int] = Query(None, description="Filter by server ID"),
session_id: Optional[str] = Query(None, description="Filter by SSH session ID"),
page: int = Query(1, ge=1),
per_page: int = Query(30, ge=1, le=500),
limit: int = Query(200, ge=1, le=500),
offset: int = Query(0, ge=0),
admin: Admin = Depends(get_current_admin),
db: AsyncSession = Depends(get_db),
):
"""List command logs with server name and admin username"""
from server.infrastructure.database.ssh_session_repo import CommandLogRepositoryImpl
from sqlalchemy import select
from server.domain.models import CommandLog, Server, Admin as AdminModel
"""Paginated command logs with server name and admin username."""
from sqlalchemy import func, select
from server.domain.models import Admin as AdminModel
from server.domain.models import CommandLog, Server
repo = CommandLogRepositoryImpl(db)
if per_page:
limit = per_page
if page and per_page:
offset = (page - 1) * per_page
filters = []
if server_id:
filters.append(CommandLog.server_id == server_id)
if session_id:
logs = await repo.get_by_session(session_id, limit)
elif server_id:
logs = await repo.get_by_server(server_id, limit)
else:
result = await db.execute(select(CommandLog).order_by(CommandLog.created_at.desc()).limit(limit))
logs = list(result.scalars().all())
filters.append(CommandLog.session_id == session_id)
count_q = select(func.count(CommandLog.id))
data_q = select(CommandLog).order_by(CommandLog.created_at.desc())
if filters:
count_q = count_q.where(*filters)
data_q = data_q.where(*filters)
total = int((await db.execute(count_q)).scalar() or 0)
result = await db.execute(data_q.offset(offset).limit(limit))
logs = list(result.scalars().all())
# Batch load server names and admin usernames
server_ids = {l.server_id for l in logs if l.server_id}
admin_ids = {l.admin_id for l in logs if l.admin_id}
@@ -291,7 +328,20 @@ async def list_command_logs(
adm_result = await db.execute(select(AdminModel.id, AdminModel.username).where(AdminModel.id.in_(admin_ids)))
admin_names = dict(adm_result.all())
return [_command_log_to_dict(l, server_names.get(l.server_id), admin_names.get(l.admin_id)) for l in logs]
import math
pages = math.ceil(total / limit) if limit and total > 0 else 1
return {
"items": [
_command_log_to_dict(l, server_names.get(l.server_id), admin_names.get(l.admin_id))
for l in logs
],
"total": total,
"page": page,
"per_page": limit,
"pages": pages,
"limit": limit,
"offset": offset,
}
# ── Helper functions ──
+82 -19
View File
@@ -16,7 +16,7 @@ import re
import socket
import bcrypt
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from pydantic import BaseModel, Field
from server.api.dependencies import get_db
@@ -812,27 +812,41 @@ alert_history_router = APIRouter(prefix="/api/alert-history", tags=["alerts"])
@alert_history_router.get("/", response_model=dict)
async def list_alert_history(
server_id: Optional[int] = None,
alert_type: Optional[str] = None,
alert_type: Optional[str] = Query(None, alias="type"),
is_recovery: Optional[bool] = None,
status: Optional[str] = Query(None, description="active|recovered — maps to is_recovery"),
date_from: Optional[str] = None,
date_to: Optional[str] = None,
limit: int = 50,
offset: int = 0,
page: Optional[int] = Query(None, ge=1),
per_page: Optional[int] = Query(None, ge=1, le=100),
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),
admin: Admin = Depends(get_current_admin),
db: AsyncSession = Depends(get_db),
):
"""Paginated alert/recovery history with filters."""
import math
from sqlalchemy import select, func
from server.domain.models import AlertLog, Server
from datetime import datetime, timezone
if per_page is not None:
limit = per_page
if page is not None and per_page is not None:
offset = (page - 1) * per_page
filters = []
if server_id:
filters.append(AlertLog.server_id == server_id)
if alert_type:
filters.append(AlertLog.alert_type == alert_type)
if is_recovery is not None:
filters.append(AlertLog.is_recovery == is_recovery)
recovery_filter = is_recovery
if status == "active":
recovery_filter = False
elif status == "recovered":
recovery_filter = True
if recovery_filter is not None:
filters.append(AlertLog.is_recovery == recovery_filter)
if date_from:
try:
dt = datetime.fromisoformat(date_from)
@@ -875,9 +889,16 @@ async def list_alert_history(
"created_at": str(log.created_at) if log.created_at else None,
}
current_page = (offset // limit) + 1 if limit else 1
pages = math.ceil(total / limit) if limit and total > 0 else 1
return {
"items": [_to_dict(log, sn) for log, sn in rows],
"total": total, "limit": limit, "offset": offset,
"total": total,
"limit": limit,
"offset": offset,
"page": page if page is not None else current_page,
"per_page": per_page if per_page is not None else limit,
"pages": pages,
}
@@ -888,7 +909,7 @@ async def alert_stats(
db: AsyncSession = Depends(get_db),
):
"""Top alerting servers + daily counts for the last N days."""
from sqlalchemy import select, func, text
from sqlalchemy import func, select
from server.domain.models import AlertLog
# Top 5 servers by alert count
@@ -911,21 +932,63 @@ async def alert_stats(
)
type_rows = (await db.execute(type_q)).all()
# Daily counts (parameterized to avoid f-string SQL)
days_val = int(days)
daily_q = text(
"SELECT DATE(created_at) as day, "
"SUM(is_recovery=0) as alerts, SUM(is_recovery=1) as recoveries "
"FROM alert_logs "
"WHERE created_at >= DATE_SUB(NOW(), INTERVAL :days DAY) "
"GROUP BY DATE(created_at) ORDER BY day"
).bindparams(days=days_val)
from datetime import date, datetime, timedelta, timezone
from sqlalchemy import case
days_val = max(1, int(days))
cutoff = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(days=days_val)
daily_q = (
select(
func.date(AlertLog.created_at).label("day"),
func.sum(case((AlertLog.is_recovery == False, 1), else_=0)).label("alerts"),
func.sum(case((AlertLog.is_recovery == True, 1), else_=0)).label("recoveries"),
)
.where(AlertLog.created_at >= cutoff)
.group_by(func.date(AlertLog.created_at))
.order_by(func.date(AlertLog.created_at))
)
daily_rows = (await db.execute(daily_q)).all()
from server.infrastructure.redis.client import get_redis
import logging
daily = [{"day": str(r.day), "alerts": int(r.alerts), "recoveries": int(r.recoveries)} for r in daily_rows]
today_str = str(date.today())
today_alerts = 0
today_recovered = 0
for row in daily:
if row["day"] == today_str:
today_alerts = row["alerts"]
today_recovered = row["recoveries"]
break
active_count = 0
try:
redis = get_redis()
cursor = 0
while True:
cursor, keys = await redis.scan(cursor, match="alerts:*", count=200)
for key in keys:
active_count += int(await redis.scard(key) or 0)
if cursor == 0:
break
except Exception as e:
logging.getLogger(__name__).warning("alert_stats: Redis active count failed: %s", e)
top_servers = [
{"server_id": r.server_id, "server_name": r.server_name or f"#{r.server_id}", "count": r.cnt}
for r in top_rows
]
top_server_name = top_servers[0]["server_name"] if top_servers else None
return {
"top_types": [{"alert_type": r.alert_type, "count": r.cnt} for r in type_rows],
"top_servers": [{"server_id": r.server_id, "server_name": r.server_name or f"#{r.server_id}", "count": r.cnt} for r in top_rows],
"daily": [{"day": str(r.day), "alerts": int(r.alerts), "recoveries": int(r.recoveries)} for r in daily_rows],
"top_servers": top_servers,
"daily": daily,
"today": today_alerts,
"active": active_count,
"recovered": today_recovered,
"top_server": top_server_name,
}
+16 -2
View File
@@ -11,7 +11,7 @@ from dataclasses import dataclass
class FeatureSpec:
id: str
name: str
layer: str # api | ui | unit | infra
layer: str # api | ui | unit | infra | integration | chain
description: str
keywords: tuple[str, ...] # 匹配子进程输出中的 [PASS]/[FAIL] 行
@@ -22,7 +22,7 @@ FEATURES: tuple[FeatureSpec, ...] = (
FeatureSpec("infra_app", "前端静态 /app/", "infra", "SPA 入口可访问", ("/app/", "static")),
FeatureSpec("auth", "认证与 JWT", "api", "登录、me、刷新", ("/api/auth/", "Auth")),
FeatureSpec("dashboard", "仪表盘统计", "api", "服务器统计与概览", ("/api/servers/stats", "stats")),
FeatureSpec("servers", "服务器管理", "api", "CRUD、分类、日志、CSV 导入", ("/api/servers/", "Server", "import")),
FeatureSpec("servers", "服务器管理", "api", "CRUD、分类、同步日志", ("/api/servers/", "Server", "import")),
FeatureSpec("files", "文件管理", "api", "browse、files-capability", ("browse", "files-capability", "sync/")),
FeatureSpec("terminal", "Web 终端", "api", "快捷命令、webssh-token", ("terminal", "webssh", "quick-commands")),
FeatureSpec("push_sync", "推送与同步", "api", "validate-source、sync", ("sync", "validate-source", "Push")),
@@ -42,6 +42,7 @@ FEATURES: tuple[FeatureSpec, ...] = (
FeatureSpec("ui_files", "页面·文件", "ui", "路由 /files", ("05 files",)),
FeatureSpec("ui_push", "页面·推送", "ui", "路由 /push", ("06 push",)),
FeatureSpec("ui_scripts", "页面·脚本", "ui", "路由 /scripts", ("07 scripts",)),
FeatureSpec("ui_executions", "页面·执行看板", "ui", "路由 /executions", ("E2E-EXEC", "executions",)),
FeatureSpec("ui_credentials", "页面·凭据", "ui", "路由 /credentials", ("08 credentials",)),
FeatureSpec("ui_schedules", "页面·调度", "ui", "路由 /schedules", ("09 schedules",)),
FeatureSpec("ui_retries", "页面·重试", "ui", "路由 /retries", ("10 retries",)),
@@ -51,6 +52,19 @@ FEATURES: tuple[FeatureSpec, ...] = (
FeatureSpec("ui_settings", "页面·设置", "ui", "路由 /settings", ("14 settings",)),
FeatureSpec("ui_theme", "页面·主题与登出", "ui", "主题切换、退出", ("15 theme",)),
FeatureSpec("unit_core", "单元测试(核心)", "unit", "路径校验、权限、提权逻辑", ("test_posix", "test_files", "test_unix", "test_remote", "test_schema")),
FeatureSpec("integration_push", "集成·推送 accepted", "integration", "IT-PUSH-001 后台化", ("test_push_sync", "IT-PUSH", "accepted")),
FeatureSpec("integration_api", "集成·API 域", "integration", "设置/凭据/审计/脚本", ("tests/integration", "integration")),
FeatureSpec("chain_push", "链条·推送进度", "chain", "CH-PUSH-001 WS", ("test_push_async", "CH-PUSH", "chain")),
FeatureSpec("chain_auth", "链条·认证", "chain", "CH-AUTH-001 login refresh", ("test_auth_flow", "CH-AUTH")),
FeatureSpec("chain_batch", "链条·批量任务", "chain", "CH-BAT-001 category job", ("test_batch_job", "CH-BAT")),
FeatureSpec("integration_files", "集成·文件浏览", "integration", "IT-FILE-001 browse JSON", ("test_files_browse", "IT-FILE")),
FeatureSpec("integration_install", "集成·安装模式", "integration", "IT-INST-001 install gate", ("test_install_mode", "IT-INST")),
FeatureSpec("ui_terminal_idle", "页面·终端 idle", "ui", "E2E-TERM-001", ("terminal.spec", "E2E-TERM")),
FeatureSpec("ui_files", "页面·文件", "ui", "E2E-FILE-001", ("files.spec", "E2E-FILE")),
FeatureSpec("ui_scripts", "页面·脚本", "ui", "E2E-SCR-001", ("scripts.spec",)),
FeatureSpec("ui_credentials", "页面·凭据校验", "ui", "E2E-CRED-001", ("credentials.spec",)),
FeatureSpec("ui_retries", "页面·重试", "ui", "E2E-RET-001", ("retries.spec",)),
FeatureSpec("ui_settings", "页面·设置", "ui", "E2E-SET-001", ("settings.spec",)),
)
+75
View File
@@ -0,0 +1,75 @@
"""CH-AGT-001 — heartbeat → Redis → dashboard online count."""
from __future__ import annotations
from unittest.mock import AsyncMock
import pytest
from server.api.agent import receive_heartbeat
from server.api.dependencies import get_server_service
from server.api.schemas import AgentHeartbeat
from server.api.servers import server_stats
pytestmark = pytest.mark.chain
@pytest.mark.asyncio
async def test_heartbeat_then_stats_online_count(
db_session,
test_server,
test_admin,
mock_request,
monkeypatch,
):
api_key = "chain-agent-key"
test_server.agent_api_key = api_key
await db_session.commit()
stored: dict[str, dict] = {}
async def fake_hset(key, mapping=None):
if mapping:
stored[key] = dict(mapping)
async def fake_scan(cursor, match=None, count=200):
keys = [k for k in stored if k.startswith("heartbeat:")]
return 0, keys
async def fake_hgetall(key):
return stored.get(key, {})
mock_redis = AsyncMock()
mock_redis.hset = fake_hset
mock_redis.expire = AsyncMock()
mock_redis.get = AsyncMock(return_value=None)
mock_redis.smembers = AsyncMock(return_value=set())
mock_redis.sadd = AsyncMock()
mock_redis.srem = AsyncMock()
mock_redis.delete = AsyncMock()
mock_redis.scan = fake_scan
mock_redis.hgetall = fake_hgetall
monkeypatch.setattr("server.api.agent.get_redis", lambda: mock_redis)
monkeypatch.setattr("server.api.servers.get_redis", lambda: mock_redis)
monkeypatch.setattr("server.api.agent.broadcast_alert", AsyncMock())
monkeypatch.setattr("server.api.agent.broadcast_recovery", AsyncMock())
monkeypatch.setattr("server.api.agent.send_telegram_system_alert", AsyncMock())
mock_request.state.db = db_session
service = await get_server_service(mock_request)
hb = await receive_heartbeat(
AgentHeartbeat(
server_id=test_server.id,
is_online=True,
system_info={"cpu_usage": 10.0, "mem_usage": 20.0, "disk_usage": 30.0},
),
api_key=api_key,
service=service,
)
assert hb["status"] == "ok"
stats = await server_stats(test_admin["admin"], db_session)
assert stats["online"] >= 1
assert stats["total"] >= 1
+36
View File
@@ -0,0 +1,36 @@
"""CH-ALT-001 — recovery detection → single telegram per metric (mock)."""
from __future__ import annotations
from unittest.mock import AsyncMock
import pytest
from server.api.agent import _detect_recovery
pytestmark = pytest.mark.chain
@pytest.mark.asyncio
async def test_recovery_flow_deduped_before_telegram(monkeypatch):
sent: list[tuple] = []
async def capture_recovery(server_id, metric, value, server_name):
sent.append((server_id, metric, value, server_name))
monkeypatch.setattr(
"server.infrastructure.telegram.send_telegram_recovery",
AsyncMock(side_effect=lambda *a, **k: sent.append(a)),
)
thresholds = {"cpu": 80, "mem": 80, "disk": 80}
prev_alerts = {"mem:88.0", "mem:91.2"}
system_info = {"mem_usage": 55.0}
recoveries = _detect_recovery(system_info, prev_alerts, thresholds)
assert recoveries == [("mem", 55.0)]
for metric, value in recoveries:
await capture_recovery(1, metric, value, "srv-1")
assert len(sent) == 1
+31
View File
@@ -0,0 +1,31 @@
"""CH-AUTH-001 — login → refresh → new access token."""
from __future__ import annotations
from unittest.mock import MagicMock
import pytest
from server.api.dependencies import get_auth_service
pytestmark = pytest.mark.chain
@pytest.mark.asyncio
async def test_login_refresh_chain(db_session, test_admin, mock_request):
mock_request.state.db = db_session
service = await get_auth_service(mock_request)
login_result = await service.login(
username=test_admin["admin"].username,
password="test123",
ip_address="127.0.0.1",
)
assert login_result["success"] is True
assert login_result.get("access_token")
refresh_raw = login_result.get("refresh_token")
assert refresh_raw
refresh_result = await service.refresh_token(refresh_raw, ip_address="127.0.0.1")
assert refresh_result["success"] is True
assert refresh_result.get("access_token")
+116
View File
@@ -0,0 +1,116 @@
"""CH-BAT-001 — batch category job → Redis live → MySQL finalize."""
from __future__ import annotations
import asyncio
import json
from unittest.mock import AsyncMock, MagicMock
import pytest
from server.application.services.server_batch_service import get_batch_job, start_batch_job
from server.infrastructure.database.server_batch_job_repo import ServerBatchJobRepositoryImpl
from server.infrastructure.database.server_repo import ServerRepositoryImpl
pytestmark = pytest.mark.chain
class _MemRedis:
"""Minimal Redis stub for server_batch_store."""
def __init__(self) -> None:
self._data: dict[str, str] = {}
self._seq = 0
async def incr(self, key: str) -> int:
self._seq += 1
self._data[key] = str(self._seq)
return self._seq
async def set(self, key: str, value: str, ex: int | None = None) -> None:
self._data[key] = value
async def get(self, key: str) -> str | None:
return self._data.get(key)
async def scan(self, cursor: int, match: str | None = None, count: int = 50):
keys = [
k
for k in self._data
if k.startswith("server_batch:") and k != "server_batch:next_id"
]
return 0, keys
class _SessionCtx:
def __init__(self, session):
self._session = session
async def __aenter__(self):
return self._session
async def __aexit__(self, exc_type, exc, tb) -> None:
if exc:
await self._session.rollback()
@pytest.mark.asyncio
async def test_batch_category_job_completes_and_persists(
db_session,
test_admin,
test_server,
monkeypatch,
):
mem = _MemRedis()
monkeypatch.setattr(
"server.infrastructure.redis.server_batch_store.get_redis",
lambda: mem,
)
monkeypatch.setattr(
"server.application.services.server_batch_service.AsyncSessionLocal",
lambda: _SessionCtx(db_session),
)
monkeypatch.setattr(
"server.application.services.server_batch_service._notify_progress",
AsyncMock(),
)
monkeypatch.setattr(
"server.application.services.server_batch_service._notify_complete",
AsyncMock(),
)
captured: list = []
def capture_create_task(coro):
captured.append(coro)
return MagicMock()
monkeypatch.setattr(asyncio, "create_task", capture_create_task)
started = await start_batch_job(
op="category",
server_ids=[test_server.id],
operator=test_admin["admin"].username,
params={"category": "chain-batch-cat"},
)
assert started["status"] == "running"
assert len(captured) == 1
await captured[0]
detail = await get_batch_job(started["job_id"])
assert detail is not None
assert detail["status"] == "completed"
assert detail["success"] == 1
assert detail["failed"] == 0
repo = ServerBatchJobRepositoryImpl(db_session)
row = await repo.get_by_id(started["job_id"])
assert row is not None
assert row.status == "completed"
assert json.loads(row.server_ids) == [test_server.id]
srv_repo = ServerRepositoryImpl(db_session)
updated = await srv_repo.get_by_id(test_server.id)
assert updated is not None
assert updated.category == "chain-batch-cat"
+31
View File
@@ -0,0 +1,31 @@
"""CH-FILE-001 — staging upload → files on disk."""
from __future__ import annotations
import pytest
from server.api.sync_v2 import _persist_staging_files
pytestmark = pytest.mark.chain
@pytest.mark.asyncio
async def test_staging_upload_then_listable(tmp_path):
staging = str(tmp_path / "nexus_upload_chain")
class FakeUpload:
def __init__(self, name: str, data: bytes):
self.filename = name
self._data = data
async def read(self):
return self._data
meta = await _persist_staging_files(
staging,
[FakeUpload("deploy.txt", b"hello-chain")],
)
assert meta["file_count"] == 1
assert (tmp_path / "nexus_upload_chain" / "deploy.txt").read_bytes() == b"hello-chain"
assert meta["source_path"] == staging
+78
View File
@@ -0,0 +1,78 @@
"""CH-PUSH-001 — POST accepted → sync WebSocket progress channel."""
from __future__ import annotations
from unittest.mock import MagicMock
import pytest
from server.api.schemas import SyncFiles
from server.api.sync_v2 import sync_files
pytestmark = pytest.mark.chain
@pytest.mark.asyncio
async def test_push_accepted_then_sync_progress_event(
db_session,
test_admin,
test_server,
monkeypatch,
):
scheduled: list[dict] = []
def fake_schedule(**kwargs):
scheduled.append(kwargs)
monkeypatch.setattr(
"server.background.sync_push_runner.schedule_sync_files",
fake_schedule,
)
monkeypatch.setattr(
"server.application.services.sync_engine_v2._nexus_source_path",
lambda _path: "/tmp/nexus-test-src",
)
request = MagicMock()
request.state.db = db_session
request.client = MagicMock()
request.client.host = "127.0.0.1"
batch_id = "112233445566"
payload = SyncFiles(
server_ids=[test_server.id],
source_path="/tmp/nexus-test-src",
batch_id=batch_id,
)
body = await sync_files(payload, request, test_admin["admin"])
assert body["accepted"] is True
assert len(scheduled) == 1
from server.api import websocket as ws
calls: list[tuple[str, dict]] = []
async def capture_sync(msg):
calls.append(("sync", msg))
async def capture_alert(msg):
calls.append(("alert", msg))
monkeypatch.setattr(ws, "_dispatch_sync_message", capture_sync)
monkeypatch.setattr(ws, "_dispatch_ws_message", capture_alert)
await ws.broadcast_sync_progress(
batch_id=batch_id,
server_id=test_server.id,
server_name=test_server.name,
status="running",
completed=0,
failed=0,
total=1,
)
assert len(calls) == 1
assert calls[0][0] == "sync"
assert calls[0][1]["type"] == "sync_progress"
assert calls[0][1]["batch_id"] == batch_id
+173
View File
@@ -0,0 +1,173 @@
"""CH-EXEC-001 — create script → execute → poll completed."""
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock
import pytest
from server.api.dependencies import get_script_service
from server.api.schemas import ScriptCreate, ScriptExecute
from server.api.scripts import create_script, execute_command, list_executions
pytestmark = pytest.mark.chain
_BG_TASKS: list = []
class _MemRedis:
def __init__(self) -> None:
self._data: dict[str, str] = {}
async def set(self, key: str, value: str, ex: int | None = None) -> None:
self._data[key] = value
async def get(self, key: str) -> str | None:
return self._data.get(key)
async def delete(self, key: str) -> None:
self._data.pop(key, None)
async def rpush(self, key: str, value: str) -> None:
self._data.setdefault(key, "[]")
async def lrange(self, key: str, start: int, end: int) -> list:
return []
async def sadd(self, key: str, member: str) -> None:
self._data.setdefault(f"set:{key}", set()).add(member)
async def srem(self, key: str, member: str) -> None:
self._data.get(f"set:{key}", set()).discard(member)
class _SessionCtx:
def __init__(self, session):
self._session = session
async def __aenter__(self):
return self._session
async def __aexit__(self, exc_type, exc, tb) -> None:
if exc:
await self._session.rollback()
def _chain_exec_env(db_session, monkeypatch) -> None:
mem = _MemRedis()
monkeypatch.setattr(
"server.infrastructure.redis.script_execution_store.get_redis",
lambda: mem,
)
monkeypatch.setattr(
"server.infrastructure.database.session.AsyncSessionLocal",
lambda: _SessionCtx(db_session),
)
monkeypatch.setattr(
"server.application.services.script_execution_notify.notify_script_progress",
AsyncMock(),
)
monkeypatch.setattr(
"server.application.services.script_execution_notify.notify_script_complete",
AsyncMock(),
)
async def fake_dispatch(self, server_ids, command, timeout=30, **kwargs):
return {
str(sid): {
"exit_code": 0,
"stdout": "ok\n",
"stderr": "",
"status": "completed",
"phase": "done",
}
for sid in server_ids
}
monkeypatch.setattr(
"server.application.services.script_service.ScriptService._dispatch_to_servers",
fake_dispatch,
)
_BG_TASKS.clear()
def capture_create_task(coro):
_BG_TASKS.append(coro)
return MagicMock()
monkeypatch.setattr(
"server.application.services.script_service.asyncio.create_task",
capture_create_task,
)
@pytest.mark.asyncio
async def test_create_script_then_list_executions(db_session, test_admin, mock_request):
mock_request.state.db = db_session
service = await get_script_service(mock_request)
created = await create_script(
ScriptCreate(name="chain-exec-script", content="echo chain", category="ops"),
mock_request,
test_admin["admin"],
service,
db_session,
)
assert created["id"] > 0
board = await list_executions(
limit=20,
offset=0,
status=None,
script_id=created["id"],
admin=test_admin["admin"],
service=service,
)
assert board["total"] >= 0
assert isinstance(board["items"], list)
@pytest.mark.asyncio
async def test_execute_script_reaches_completed(
db_session,
test_admin,
test_server,
mock_request,
monkeypatch,
):
_chain_exec_env(db_session, monkeypatch)
mock_request.state.db = db_session
service = await get_script_service(mock_request)
created = await create_script(
ScriptCreate(name="chain-exec-run", content="echo done", category="ops"),
mock_request,
test_admin["admin"],
service,
db_session,
)
started = await execute_command(
ScriptExecute(script_id=created["id"], server_ids=[test_server.id], timeout=30),
test_admin["admin"],
service,
)
assert started["status"] == "running"
assert started["id"] > 0
assert len(_BG_TASKS) == 1
await _BG_TASKS[0]
detail = await service.get_execution_detail(started["id"])
assert detail is not None
assert detail["status"] == "completed"
assert detail["results"][str(test_server.id)]["exit_code"] == 0
from server.infrastructure.redis import script_execution_store as ses
live = await ses.load_live(started["id"])
assert live is not None
await ses.flush_to_mysql(service.execution_repo, live)
row = await service.get_execution(started["id"])
assert row is not None
assert row.status == "completed"
+49
View File
@@ -0,0 +1,49 @@
"""CH-SRV-001 — list server → update field → persisted."""
from __future__ import annotations
from unittest.mock import AsyncMock
import pytest
from server.api.dependencies import get_server_service
from server.api.schemas import ServerUpdate
from server.api.servers import get_server, update_server
from server.infrastructure.database.server_repo import ServerRepositoryImpl
pytestmark = pytest.mark.chain
@pytest.mark.asyncio
async def test_server_list_update_persisted(
db_session,
test_admin,
test_server,
mock_request,
monkeypatch,
):
mock_request.state.db = db_session
service = await get_server_service(mock_request)
mock_redis = AsyncMock()
mock_redis.hgetall = AsyncMock(return_value={})
monkeypatch.setattr("server.api.servers.get_redis", lambda: mock_redis)
detail = await get_server(test_server.id, test_admin["admin"], service)
assert detail["id"] == test_server.id
new_desc = "chain-integration-desc"
updated = await update_server(
mock_request,
test_server.id,
ServerUpdate(description=new_desc),
test_admin["admin"],
service,
db_session,
)
assert updated.get("description") == new_desc
repo = ServerRepositoryImpl(db_session)
row = await repo.get_by_id(test_server.id)
assert row is not None
assert row.description == new_desc
+98 -10
View File
@@ -2,20 +2,49 @@
Fixed: wired up dependency_overrides properly,
added test Redis mock, added admin fixture for JWT auth tests.
Test layers (see docs/testing/test-case-matrix.md):
unit / integration / chain / slow — register via pytest_configure below.
"""
import asyncio
import pytest
from httpx import AsyncClient, ASGITransport
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
import uuid
import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from server.domain.models import Base, Admin
from server.config import settings
from server.domain.models import Admin, Base, Server
def pytest_configure(config):
config.addinivalue_line("markers", "unit: L1 isolated logic (docs/testing/test-case-matrix.md)")
config.addinivalue_line("markers", "integration: L2 API + DB/Redis")
config.addinivalue_line("markers", "chain: L3 multi-step business flows")
config.addinivalue_line("markers", "slow: >5s or needs live stack; skip in fast CI")
# Use in-memory SQLite for unit/integration tests
TEST_DATABASE_URL = "sqlite+aiosqlite://"
class _RequestSessionCtx:
"""New SQLite session per HTTP request (avoids MissingGreenlet)."""
def __init__(self, session_factory):
self._session_factory = session_factory
async def __aenter__(self) -> AsyncSession:
self._session = self._session_factory()
return self._session
async def __aexit__(self, exc_type, exc, tb) -> None:
if exc:
await self._session.rollback()
await self._session.close()
@pytest.fixture(scope="session")
def event_loop():
loop = asyncio.new_event_loop()
@@ -23,7 +52,7 @@ def event_loop():
loop.close()
@pytest.fixture(scope="session")
@pytest_asyncio.fixture(scope="function")
async def test_engine():
engine = create_async_engine(TEST_DATABASE_URL, echo=False)
async with engine.begin() as conn:
@@ -34,9 +63,40 @@ async def test_engine():
await engine.dispose()
@pytest.fixture(scope="function")
@pytest.fixture
def auth_headers(test_admin):
return {"Authorization": f"Bearer {test_admin['token']}"}
@pytest.fixture
def integration_env(test_engine, test_admin, monkeypatch):
"""Wire middleware DB + JWT to in-memory fixtures (IT/CH tests)."""
session_factory = async_sessionmaker(
test_engine,
class_=AsyncSession,
expire_on_commit=False,
)
monkeypatch.setattr(
"server.main.AsyncSessionLocal",
lambda: _RequestSessionCtx(session_factory),
)
admin = test_admin["admin"]
token = test_admin["token"]
async def fake_verify(jwt_token, request):
return admin if jwt_token == token else None
monkeypatch.setattr("server.api.auth_jwt._verify_token", fake_verify)
@pytest_asyncio.fixture(scope="function")
async def db_session(test_engine):
session_factory = async_sessionmaker(test_engine, class_=AsyncSession)
session_factory = async_sessionmaker(
test_engine,
class_=AsyncSession,
expire_on_commit=False,
)
session = session_factory()
try:
yield session
@@ -44,7 +104,7 @@ async def db_session(test_engine):
await session.close()
@pytest.fixture(scope="function")
@pytest_asyncio.fixture(scope="function")
async def api_client(db_session):
"""Create an async HTTP client for API integration tests"""
from server.main import app
@@ -62,7 +122,7 @@ async def api_client(db_session):
app.dependency_overrides.clear()
@pytest.fixture
@pytest_asyncio.fixture
async def test_admin(db_session):
"""Create a test admin user and return with JWT token"""
import bcrypt
@@ -76,7 +136,7 @@ async def test_admin(db_session):
password_hash = bcrypt.hashpw("test123".encode(), bcrypt.gensalt()).decode()
admin = Admin(
username="testadmin",
username=f"testadmin_{uuid.uuid4().hex[:8]}",
password_hash=password_hash,
is_active=True,
totp_enabled=False,
@@ -93,3 +153,31 @@ async def test_admin(db_session):
token = auth_service._create_access_token(admin)
return {"admin": admin, "token": token}
@pytest.fixture
def mock_request(db_session):
"""Minimal FastAPI Request for direct route-handler tests."""
from unittest.mock import MagicMock
request = MagicMock()
request.state.db = db_session
request.client = MagicMock()
request.client.host = "127.0.0.1"
request.cookies = {}
return request
@pytest_asyncio.fixture
async def test_server(db_session):
"""Single server with target_path for push/sync integration tests."""
server = Server(
name=f"test-push-srv-{uuid.uuid4().hex[:8]}",
domain="10.0.0.99",
target_path="/www/wwwroot",
is_online=True,
)
db_session.add(server)
await db_session.commit()
await db_session.refresh(server)
return server
+6
View File
@@ -0,0 +1,6 @@
# L2 集成测试
API + 内存 SQLite / mock Redis;复杂场景用 `api_client` + `test_admin` fixtures。
- 编号:`IT-<域>-<序号>`
- 运行:`pytest tests/integration -m integration -q`
+51
View File
@@ -0,0 +1,51 @@
"""IT-AGT-001 — agent heartbeat accepts valid per-server API key."""
from __future__ import annotations
from unittest.mock import AsyncMock
import pytest
from server.api.agent import receive_heartbeat
from server.api.dependencies import get_server_service
from server.api.schemas import AgentHeartbeat
pytestmark = pytest.mark.integration
@pytest.mark.asyncio
async def test_heartbeat_ok_with_server_key(
db_session,
test_server,
mock_request,
monkeypatch,
):
api_key = "test-agent-key-001"
test_server.agent_api_key = api_key
await db_session.commit()
mock_redis = AsyncMock()
mock_redis.set = AsyncMock()
mock_redis.get = AsyncMock(return_value=None)
mock_redis.smembers = AsyncMock(return_value=set())
mock_redis.sadd = AsyncMock()
mock_redis.srem = AsyncMock()
mock_redis.delete = AsyncMock()
monkeypatch.setattr("server.api.agent.get_redis", lambda: mock_redis)
monkeypatch.setattr("server.api.agent.broadcast_alert", AsyncMock())
monkeypatch.setattr("server.api.agent.broadcast_recovery", AsyncMock())
monkeypatch.setattr("server.api.agent.send_telegram_system_alert", AsyncMock())
mock_request.state.db = db_session
service = await get_server_service(mock_request)
payload = AgentHeartbeat(
server_id=test_server.id,
is_online=True,
system_info={"cpu_usage": 10.0, "mem_usage": 20.0, "disk_usage": 30.0},
agent_version="1.0.0",
)
result = await receive_heartbeat(payload, api_key=api_key, service=service)
assert result.get("status") in ("ok", "received", "success") or "server_id" in result
+88
View File
@@ -0,0 +1,88 @@
"""IT-AUD-001 / IT-ALT-001 — audit and alert history pagination."""
from __future__ import annotations
import pytest
from server.api.settings import alert_stats, list_alert_history, list_audit_logs
from server.domain.models import AuditLog
pytestmark = pytest.mark.integration
@pytest.mark.asyncio
async def test_audit_logs_paginated(db_session, test_admin):
db_session.add(
AuditLog(
admin_username=test_admin["admin"].username,
action="test_action",
target_type="test",
target_id=1,
detail="integration test",
)
)
await db_session.commit()
body = await list_audit_logs(
action=None,
admin_username=None,
date_from=None,
date_to=None,
limit=10,
offset=0,
admin=test_admin["admin"],
db=db_session,
)
assert body["total"] >= 1
assert len(body["items"]) >= 1
assert body["items"][0]["action"] == "test_action"
@pytest.mark.asyncio
async def test_alert_history_list_200(db_session, test_admin):
body = await list_alert_history(
server_id=None,
alert_type=None,
is_recovery=None,
status=None,
date_from=None,
date_to=None,
page=1,
per_page=10,
limit=10,
offset=0,
admin=test_admin["admin"],
db=db_session,
)
assert "items" in body
assert "total" in body
assert body["page"] == 1
assert body["per_page"] == 10
@pytest.mark.asyncio
async def test_alert_history_status_filter_alias(db_session, test_admin):
body = await list_alert_history(
server_id=None,
alert_type=None,
is_recovery=None,
status="active",
date_from=None,
date_to=None,
page=None,
per_page=None,
limit=50,
offset=0,
admin=test_admin["admin"],
db=db_session,
)
assert "items" in body
for item in body["items"]:
assert item["is_recovery"] is False
@pytest.mark.asyncio
async def test_alert_stats_frontend_fields(db_session, test_admin):
stats = await alert_stats(days=7, admin=test_admin["admin"], db=db_session)
for key in ("today", "active", "recovered", "top_server", "top_types", "top_servers", "daily"):
assert key in stats
+53
View File
@@ -0,0 +1,53 @@
"""IT-FILE-001 — GET /api/files/browse returns directory JSON."""
from __future__ import annotations
import json
from unittest.mock import AsyncMock, patch
import pytest
from server.api.files import browse_directory_get
pytestmark = pytest.mark.integration
@pytest.mark.asyncio
async def test_browse_directory_get_returns_items(
db_session,
test_admin,
test_server,
mock_request,
):
mock_request.state.db = db_session
payload = {
"path": "/www/wwwroot",
"items": [
{
"name": "index.html",
"is_dir": False,
"size": 1024,
"mtime": "2026-06-08 12:00",
}
],
}
with patch(
"server.api.files.list_remote_directory",
new_callable=AsyncMock,
return_value=payload,
):
response = await browse_directory_get(
mock_request,
server_id=test_server.id,
path="/www/wwwroot",
admin=test_admin["admin"],
if_none_match=None,
)
assert response.status_code == 200
body = json.loads(response.body)
assert body["path"] == "/www/wwwroot"
assert len(body["items"]) == 1
assert body["items"][0]["name"] == "index.html"
assert "ETag" in response.headers
+37
View File
@@ -0,0 +1,37 @@
"""IT-INST-001 — install mode allows /health + install API, blocks business API."""
from __future__ import annotations
from unittest.mock import patch
import pytest
from httpx import ASGITransport, AsyncClient
pytestmark = [pytest.mark.integration, pytest.mark.slow]
@pytest.mark.asyncio
async def test_install_mode_health_ok_servers_blocked(monkeypatch):
monkeypatch.setattr("server.main.is_install_mode", lambda: True)
monkeypatch.setattr("server.api.auth_jwt._is_install_mode", lambda: True)
from server.main import app
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
health = await client.get("/health")
assert health.status_code == 200
servers = await client.get("/api/servers/")
assert servers.status_code == 503
assert "安装" in servers.json()["detail"]
with patch("server.api.install._is_locked", return_value=False), patch(
"server.api.install._has_env",
return_value=False,
):
status = await client.get("/api/install/status")
assert status.status_code == 200
body = status.json()
assert body["configured"] is False
assert body["locked"] is False
+69
View File
@@ -0,0 +1,69 @@
"""IT-SRV-003 — notify toggle via settings API with audit."""
from __future__ import annotations
from unittest.mock import AsyncMock, patch
import pytest
from server.api.schemas import SettingUpdatePayload
from server.api.settings import set_setting
from server.domain.models import Setting
from server.infrastructure.database.audit_log_repo import AuditLogRepositoryImpl
from server.infrastructure.database.setting_repo import SettingRepositoryImpl
async def _sqlite_compatible_set(self, key: str, value: str) -> Setting:
row = await self.session.get(Setting, key)
if row is None:
row = Setting(key=key, value=value)
self.session.add(row)
else:
row.value = value
await self.session.commit()
await self.session.refresh(row)
return row
pytestmark = pytest.mark.integration
@pytest.mark.asyncio
async def test_notify_alert_cpu_toggle_writes_audit(
db_session,
test_admin,
mock_request,
monkeypatch,
):
db_session.add(Setting(key="notify_alert_cpu", value="true"))
await db_session.commit()
monkeypatch.setattr("server.config.settings.NOTIFY_ALERT_CPU", "true")
monkeypatch.setattr(SettingRepositoryImpl, "set", _sqlite_compatible_set)
with patch(
"server.infrastructure.settings_broadcast.publish_setting_change",
new_callable=AsyncMock,
) as mock_publish:
result = await set_setting(
"notify_alert_cpu",
SettingUpdatePayload(value="false"),
mock_request,
test_admin["admin"],
db_session,
)
assert result["key"] == "notify_alert_cpu"
assert result["value"] == "false"
mock_publish.assert_awaited_once_with("notify_alert_cpu", "false")
setting = await db_session.get(Setting, "notify_alert_cpu")
assert setting is not None
assert setting.value == "false"
audit_repo = AuditLogRepositoryImpl(db_session)
logs, total = await audit_repo.query(action="update_setting", limit=5, offset=0)
assert total >= 1
assert any(
log.action == "update_setting" and "notify_alert_cpu" in (log.detail or "")
for log in logs
)
+29
View File
@@ -0,0 +1,29 @@
"""IT-CRED-001 — preset list never returns encrypted password."""
from __future__ import annotations
import pytest
from server.api.settings import list_presets
from server.domain.models import PasswordPreset
from server.infrastructure.database.crypto import encrypt_value
pytestmark = pytest.mark.integration
@pytest.mark.asyncio
async def test_list_presets_masks_encrypted_pw(db_session, test_admin):
preset = PasswordPreset(
name="e2e-preset",
username="root",
encrypted_pw=encrypt_value("secret-pass"),
)
db_session.add(preset)
await db_session.commit()
rows = await list_presets(test_admin["admin"], db_session)
assert len(rows) >= 1
for row in rows:
assert "encrypted_pw" not in row
assert "password" not in row
+57
View File
@@ -0,0 +1,57 @@
"""IT-PUSH-001 — POST /api/sync/files returns accepted immediately."""
from __future__ import annotations
from unittest.mock import MagicMock
import pytest
from server.api.schemas import SyncFiles
from server.api.sync_v2 import sync_files
pytestmark = pytest.mark.integration
@pytest.mark.asyncio
async def test_sync_files_returns_accepted_without_blocking(
db_session,
test_admin,
test_server,
monkeypatch,
):
scheduled: list[dict] = []
def fake_schedule(**kwargs):
scheduled.append(kwargs)
monkeypatch.setattr(
"server.background.sync_push_runner.schedule_sync_files",
fake_schedule,
)
monkeypatch.setattr(
"server.application.services.sync_engine_v2._nexus_source_path",
lambda _path: "/tmp/nexus-test-src",
)
request = MagicMock()
request.state.db = db_session
request.client = MagicMock()
request.client.host = "127.0.0.1"
batch_id = "aabbccddeeff"
payload = SyncFiles(
server_ids=[test_server.id],
source_path="/tmp/nexus-test-src",
sync_mode="incremental",
batch_id=batch_id,
)
body = await sync_files(payload, request, test_admin["admin"])
assert body["accepted"] is True
assert body["batch_id"] == batch_id
assert body["total"] == 1
assert body["completed"] == 0
assert len(scheduled) == 1
assert scheduled[0]["batch_id"] == batch_id
assert scheduled[0]["server_ids"] == [test_server.id]
@@ -0,0 +1,47 @@
"""IT-RET-001 / IT-CMD-001 — retries queue and assets command logs."""
from __future__ import annotations
import pytest
from server.api.settings import list_retry_jobs
pytestmark = pytest.mark.integration
@pytest.mark.asyncio
async def test_list_retry_jobs(db_session, test_admin):
jobs = await list_retry_jobs(limit=50, admin=test_admin["admin"], db=db_session)
assert isinstance(jobs, list)
@pytest.mark.asyncio
async def test_assets_command_logs_paginated(db_session, test_admin):
from server.api.assets import list_command_logs, list_ssh_sessions
body = await list_command_logs(
server_id=None,
session_id=None,
page=1,
per_page=5,
limit=5,
offset=0,
admin=test_admin["admin"],
db=db_session,
)
assert "items" in body
assert "total" in body
assert body["page"] == 1
assert body["per_page"] == 5
sessions = await list_ssh_sessions(
server_id=None,
page=1,
per_page=5,
limit=5,
offset=0,
admin=test_admin["admin"],
db=db_session,
)
assert "items" in sessions
assert "total" in sessions
+43
View File
@@ -0,0 +1,43 @@
"""IT-SCH-001 — schedule CRUD with audit."""
from __future__ import annotations
import pytest
from server.api.schemas import ScheduleCreate, ScheduleUpdate
from server.api.settings import create_schedule, delete_schedule, list_schedules, update_schedule
pytestmark = pytest.mark.integration
@pytest.mark.asyncio
async def test_schedule_crud_minimal(db_session, test_admin, test_server, mock_request):
payload = ScheduleCreate(
name="it-schedule-test",
server_ids=f"[{test_server.id}]",
run_mode="cron",
cron_expr="0 3 * * *",
schedule_type="push",
source_path="/tmp/nexus-schedule-src",
target_path="/www/wwwroot",
)
created = await create_schedule(payload, mock_request, test_admin["admin"], db_session)
assert created["id"] > 0
assert created["name"] == "it-schedule-test"
rows = await list_schedules(test_admin["admin"], db_session)
assert any(r["id"] == created["id"] for r in rows)
updated = await update_schedule(
created["id"],
ScheduleUpdate(name="it-schedule-renamed"),
mock_request,
test_admin["admin"],
db_session,
)
assert updated["name"] == "it-schedule-renamed"
await delete_schedule(created["id"], mock_request, test_admin["admin"], db_session)
rows_after = await list_schedules(test_admin["admin"], db_session)
assert not any(r["id"] == created["id"] for r in rows_after)
@@ -0,0 +1,59 @@
"""IT-EXEC-001 / IT-SCR-001 — script CRUD and execution list."""
from __future__ import annotations
import pytest
from server.api.dependencies import get_script_service
from server.api.schemas import ScriptCreate
from server.api.scripts import create_script, list_executions, list_scripts
from server.domain.models import Script
pytestmark = pytest.mark.integration
async def _script_service(db_session, mock_request):
mock_request.state.db = db_session
return await get_script_service(mock_request)
@pytest.mark.asyncio
async def test_list_executions_empty(db_session, test_admin, mock_request):
service = await _script_service(db_session, mock_request)
body = await list_executions(
limit=10,
offset=0,
status=None,
script_id=None,
admin=test_admin["admin"],
service=service,
)
assert "items" in body
assert "total" in body
@pytest.mark.asyncio
async def test_script_crud_minimal(db_session, test_admin, mock_request):
service = await _script_service(db_session, mock_request)
payload = ScriptCreate(name="it-test-script", content="echo ok", category="ops")
created = await create_script(
payload,
mock_request,
test_admin["admin"],
service,
db_session,
)
assert created["name"] == "it-test-script"
assert created["id"] > 0
scripts = await list_scripts(
category=None,
admin=test_admin["admin"],
service=service,
)
names = [s["name"] for s in scripts]
assert "it-test-script" in names
row = await db_session.get(Script, created["id"])
assert row is not None
@@ -0,0 +1,45 @@
"""IT-DASH-001 / IT-PUSH-002 — dashboard stats and sync logs pagination."""
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock
import pytest
from server.api.servers import get_all_sync_logs, server_stats
pytestmark = pytest.mark.integration
@pytest.mark.asyncio
async def test_server_stats_returns_counts(db_session, test_admin, test_server, monkeypatch):
async def fake_scan(cursor, match=None, count=200):
return 0, []
mock_redis = AsyncMock()
mock_redis.scan = fake_scan
monkeypatch.setattr("server.api.servers.get_redis", lambda: mock_redis)
body = await server_stats(test_admin["admin"], db_session)
assert "total" in body
assert body["total"] >= 1
assert "online" in body
assert "categories" in body
@pytest.mark.asyncio
async def test_sync_logs_paginated(db_session, test_admin):
body = await get_all_sync_logs(
server_id=None,
status=None,
sync_mode=None,
trigger_type=None,
page=1,
per_page=20,
admin=test_admin["admin"],
db=db_session,
)
assert "items" in body
assert "total" in body
assert isinstance(body["items"], list)
@@ -0,0 +1,25 @@
"""IT-SET-001 — immutable settings rejected via API."""
from __future__ import annotations
import pytest
from fastapi import HTTPException
from server.api.schemas import SettingUpdatePayload
from server.api.settings import set_setting
pytestmark = pytest.mark.integration
@pytest.mark.asyncio
async def test_put_api_key_rejected_as_immutable(db_session, test_admin, mock_request):
with pytest.raises(HTTPException) as exc:
await set_setting(
"api_key",
SettingUpdatePayload(value="hacked"),
mock_request,
test_admin["admin"],
db_session,
)
assert exc.value.status_code == 403
assert "immutable" in exc.value.detail.lower()
+36
View File
@@ -0,0 +1,36 @@
"""IT-TERM-001 — WebSSH WebSocket rejects missing/invalid token."""
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock
import pytest
from server.api.webssh import terminal_ws
pytestmark = pytest.mark.integration
@pytest.mark.asyncio
async def test_terminal_ws_closes_without_token():
ws = AsyncMock()
ws.close = AsyncMock()
ws.client = MagicMock()
ws.client.host = "127.0.0.1"
await terminal_ws(ws, server_id=1, token=None)
ws.close.assert_awaited_once_with(code=4001, reason="Missing JWT token")
ws.accept.assert_not_called()
@pytest.mark.asyncio
async def test_terminal_ws_closes_with_invalid_token():
ws = AsyncMock()
ws.close = AsyncMock()
ws.client = MagicMock()
ws.client.host = "127.0.0.1"
await terminal_ws(ws, server_id=1, token="not-a-valid-jwt")
ws.close.assert_awaited_once_with(code=4401, reason="Invalid or expired JWT token")
+1 -1
View File
@@ -1,6 +1,6 @@
"""Batch agent result server name resolution."""
from server.api.servers import _batch_server_display_name
from server.application.server_batch_common import batch_server_display_name as _batch_server_display_name
from server.domain.models import Server
+6
View File
@@ -0,0 +1,6 @@
# L1 单元测试
新用例写在此目录;现有 `tests/test_*.py` 逐步归类迁入(不阻塞门控)。
- 编号:`UT-<域>-<序号>``docs/testing/test-case-matrix.md`
- 运行:`pytest tests/unit -m unit -q`
-1
View File
@@ -1 +0,0 @@
import{A as e,Er as t,F as n,N as r,Nr as i,Ur as a,Z as o,Zn as s,_r as c,ar as l,er as u,hr as d,k as f,or as p,rr as m,sr as h,tr as g,w as _,yr as v,zr as y,zt as b}from"./index-HeC-ymfd.js";import{o as x,t as S}from"./VContainer-Bqb4OsjX.js";import{n as C}from"./dataTable-esDY4cOk.js";import{t as w}from"./VChip-C-HpH-oj.js";import{t as T}from"./VSelect-CEXOntEU.js";import{t as E}from"./VDataTableServer-_AO1gvgs.js";import{n as D,t as O}from"./VRow-w4SO-kR5.js";import{t as k}from"./VSpacer-C31iLuvS.js";var A={class:`font-weight-medium`},j={class:`text-body-2`},M={class:`text-caption text-medium-emphasis`},N=h({__name:`AlertsPage`,setup(h){let N=x(),P=i(!1),F=i([]),I=i(0),L=i(1),R=i(null),z=i(null),B=i([{label:`今日告警`,value:`0`,color:`blue`,icon:`mdi-alert`},{label:`活跃告警`,value:`0`,color:`orange`,icon:`mdi-bell-ring`},{label:`已恢复`,value:`0`,color:`green`,icon:`mdi-check-circle`},{label:`Top 服务器`,value:``,color:`red`,icon:`mdi-server`}]),V=[{label:`CPU`,value:`cpu`},{label:`内存`,value:`memory`},{label:`磁盘`,value:`disk`},{label:`连接`,value:`connection`}],H=[{label:`活跃`,value:`active`},{label:`已恢复`,value:`recovered`}],U=C([{title:`类型`,key:`alert_type`,width:100},{title:`服务器`,key:`server_name`},{title:`详情`,key:`value`},{title:`状态`,key:`is_recovery`,width:100},{title:`时间`,key:`created_at`,width:160}]);async function W(){try{let e=await b.get(`/alert-history/stats`);B.value[0].value=String(e.today||0),B.value[1].value=String(e.active||0),B.value[2].value=String(e.recovered||0),B.value[3].value=e.top_server||``}catch{N(`加载统计失败`,`error`)}}async function G(){P.value=!0;try{let e=await b.get(`/alert-history/`,{page:L.value,per_page:20,type:R.value||void 0,status:z.value||void 0});F.value=e.items||[],I.value=e.total||0}catch{F.value=[]}finally{P.value=!1}}function K(e){return e===`cpu`||e===`CPU`?`error`:e===`memory`||e===`内存`?`warning`:e===`disk`||e===`磁盘`?`info`:`primary`}function q(e){return e===`cpu`||e===`CPU`?`mdi-chip`:e===`memory`||e===`内存`?`mdi-memory`:e===`disk`||e===`磁盘`?`mdi-harddisk`:`mdi-lan-disconnect`}return d(()=>{W(),G()}),(i,d)=>(c(),g(S,{fluid:``,class:`pa-6`},{default:t(()=>[p(O,{class:`mb-4`},{default:t(()=>[(c(!0),m(s,null,v(B.value,n=>(c(),g(D,{cols:`12`,sm:`6`,lg:`3`,key:n.label},{default:t(()=>[p(_,{elevation:`0`,lines:`two`,rounded:`lg`,border:``},{default:t(()=>[p(f,null,{append:t(()=>[p(o,{color:n.color,size:`30`},{default:t(()=>[l(a(n.icon),1)]),_:2},1032,[`color`])]),default:t(()=>[p(e,{class:`text-body-small`},{default:t(()=>[l(a(n.label),1)]),_:2},1024),p(e,null,{default:t(()=>[l(a(n.value),1)]),_:2},1024)]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1}),p(r,{elevation:`0`,border:``,rounded:`lg`},{default:t(()=>[p(n,{class:`d-flex align-center`},{default:t(()=>[d[5]||=l(` 告警历史 `,-1),p(k),p(T,{modelValue:R.value,"onUpdate:modelValue":[d[0]||=e=>R.value=e,d[1]||=e=>{L.value=1,G()}],items:V,"item-title":`label`,"item-value":`value`,label:`类型`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},clearable:``},null,8,[`modelValue`]),p(T,{modelValue:z.value,"onUpdate:modelValue":[d[2]||=e=>z.value=e,d[3]||=e=>{L.value=1,G()}],items:H,"item-title":`label`,"item-value":`value`,label:`状态`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},class:`ml-3`,clearable:``},null,8,[`modelValue`])]),_:1}),p(E,{items:F.value,headers:y(U),"items-length":I.value,loading:P.value,page:L.value,"items-per-page":20,hover:``,density:`comfortable`,"onUpdate:page":d[4]||=e=>{L.value=e,G()}},{"item.alert_type":t(({item:e})=>[p(w,{color:K(e.alert_type),size:`small`,variant:`tonal`,label:``},{prepend:t(()=>[p(o,{size:`12`},{default:t(()=>[l(a(q(e.alert_type)),1)]),_:2},1024)]),default:t(()=>[l(` `+a(e.alert_type),1)]),_:2},1032,[`color`])]),"item.server_name":t(({item:e})=>[u(`span`,A,a(e.server_name),1)]),"item.value":t(({item:e})=>[u(`span`,j,a(e.value),1)]),"item.is_recovery":t(({item:e})=>[e.is_recovery?(c(),g(w,{key:0,color:`success`,size:`small`,variant:`tonal`,label:``},{default:t(()=>[...d[6]||=[l(`已恢复`,-1)]]),_:1})):(c(),g(w,{key:1,color:`error`,size:`small`,variant:`tonal`,label:``},{default:t(()=>[...d[7]||=[l(`告警中`,-1)]]),_:1}))]),"item.created_at":t(({item:e})=>[u(`span`,M,a(e.created_at),1)]),"no-data":t(()=>[...d[8]||=[u(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`headers`,`items-length`,`loading`,`page`])]),_:1})]),_:1}))}});export{N as default};
-1
View File
@@ -1 +0,0 @@
import{$n as e,A as t,Ar as n,Br as r,Cr as i,F as a,N as o,Rt as s,X as c,Xn as l,_r as u,ar as d,er as f,hr as p,ir as m,k as h,nr as g,or as _,pr as v,w as y}from"./index-D2EJvj3F.js";import{o as b,t as x}from"./VContainer-V0GVtfac.js";import{t as S}from"./VChip-CxvvqaNz.js";import{t as C}from"./VSelect-D51DucO3.js";import{t as w}from"./VDataTableServer-R2rypyon.js";import{n as T,t as E}from"./VRow-3BRmb9CU.js";import{t as D}from"./VSpacer-BTC8O16o.js";var O={class:`font-weight-medium`},k={class:`text-body-2`},A={class:`text-caption text-medium-emphasis`},j=_({__name:`AlertsPage`,setup(_){let j=b(),M=n(!1),N=n([]),P=n(0),F=n(1),I=n(null),L=n(null),R=n([{label:`今日告警`,value:`0`,color:`blue`,icon:`mdi-alert`},{label:`活跃告警`,value:`0`,color:`orange`,icon:`mdi-bell-ring`},{label:`已恢复`,value:`0`,color:`green`,icon:`mdi-check-circle`},{label:`Top 服务器`,value:``,color:`red`,icon:`mdi-server`}]),z=[{label:`CPU`,value:`cpu`},{label:`内存`,value:`memory`},{label:`磁盘`,value:`disk`},{label:`连接`,value:`connection`}],B=[{label:`活跃`,value:`active`},{label:`已恢复`,value:`recovered`}],V=[{title:`类型`,key:`alert_type`,width:100},{title:`服务器`,key:`server_name`},{title:`详情`,key:`value`},{title:`状态`,key:`is_recovery`,width:100},{title:`时间`,key:`created_at`,width:160}];async function H(){try{let e=await s.get(`/alert-history/stats`);R.value[0].value=String(e.today||0),R.value[1].value=String(e.active||0),R.value[2].value=String(e.recovered||0),R.value[3].value=e.top_server||``}catch{j(`加载统计失败`,`error`)}}async function U(){M.value=!0;try{let e=await s.get(`/alert-history/`,{page:F.value,per_page:20,type:I.value||void 0,status:L.value||void 0});N.value=e.items||[],P.value=e.total||0}catch{N.value=[]}finally{M.value=!1}}function W(e){return e===`cpu`||e===`CPU`?`error`:e===`memory`||e===`内存`?`warning`:e===`disk`||e===`磁盘`?`info`:`primary`}function G(e){return e===`cpu`||e===`CPU`?`mdi-chip`:e===`memory`||e===`内存`?`mdi-memory`:e===`disk`||e===`磁盘`?`mdi-harddisk`:`mdi-lan-disconnect`}return v(()=>{H(),U()}),(n,s)=>(p(),f(x,{fluid:``,class:`pa-6`},{default:i(()=>[d(E,{class:`mb-4`},{default:i(()=>[(p(!0),g(l,null,u(R.value,e=>(p(),f(T,{cols:`12`,sm:`6`,lg:`3`,key:e.label},{default:i(()=>[d(y,{elevation:`0`,lines:`two`,rounded:`lg`,border:``},{default:i(()=>[d(h,null,{append:i(()=>[d(c,{color:e.color,size:`30`},{default:i(()=>[m(r(e.icon),1)]),_:2},1032,[`color`])]),default:i(()=>[d(t,{class:`text-body-small`},{default:i(()=>[m(r(e.label),1)]),_:2},1024),d(t,null,{default:i(()=>[m(r(e.value),1)]),_:2},1024)]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1}),d(o,{elevation:`0`,border:``,rounded:`lg`},{default:i(()=>[d(a,{class:`d-flex align-center`},{default:i(()=>[s[5]||=m(` 告警历史 `,-1),d(D),d(C,{modelValue:I.value,"onUpdate:modelValue":[s[0]||=e=>I.value=e,s[1]||=e=>{F.value=1,U()}],items:z,"item-title":`label`,"item-value":`value`,label:`类型`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},clearable:``},null,8,[`modelValue`]),d(C,{modelValue:L.value,"onUpdate:modelValue":[s[2]||=e=>L.value=e,s[3]||=e=>{F.value=1,U()}],items:B,"item-title":`label`,"item-value":`value`,label:`状态`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},class:`ml-3`,clearable:``},null,8,[`modelValue`])]),_:1}),d(w,{items:N.value,headers:V,"items-length":P.value,loading:M.value,page:F.value,"items-per-page":20,hover:``,density:`comfortable`,"onUpdate:page":s[4]||=e=>{F.value=e,U()}},{"item.alert_type":i(({item:e})=>[d(S,{color:W(e.alert_type),size:`small`,variant:`tonal`,label:``},{prepend:i(()=>[d(c,{size:`12`},{default:i(()=>[m(r(G(e.alert_type)),1)]),_:2},1024)]),default:i(()=>[m(` `+r(e.alert_type),1)]),_:2},1032,[`color`])]),"item.server_name":i(({item:t})=>[e(`span`,O,r(t.server_name),1)]),"item.value":i(({item:t})=>[e(`span`,k,r(t.value),1)]),"item.is_recovery":i(({item:e})=>[e.is_recovery?(p(),f(S,{key:0,color:`success`,size:`small`,variant:`tonal`,label:``},{default:i(()=>[...s[6]||=[m(`已恢复`,-1)]]),_:1})):(p(),f(S,{key:1,color:`error`,size:`small`,variant:`tonal`,label:``},{default:i(()=>[...s[7]||=[m(`告警中`,-1)]]),_:1}))]),"item.created_at":i(({item:t})=>[e(`span`,A,r(t.created_at),1)]),"no-data":i(()=>[...s[8]||=[e(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`items-length`,`loading`,`page`])]),_:1})]),_:1}))}});export{j as default};
-1
View File
@@ -1 +0,0 @@
import{$n as e,A as t,F as n,N as r,Rt as i,Vr as a,X as o,Xn as s,ar as c,er as l,gr as u,ir as d,jr as f,k as p,mr as m,nr as h,sr as g,vr as _,w as v,wr as y}from"./index-C7H1337X.js";import{t as b}from"./useSnackbar-D7YOfKok.js";import{a as x}from"./VSelectionControl-9ZrGsJjb.js";import{n as S,t as C}from"./VRow-BtPHEX01.js";import{t as w}from"./VSpacer-Ch9ZS-RX.js";import{t as T}from"./VChip-Duayi_My.js";import{t as E}from"./VSelect-DQk52RUt.js";import{t as D}from"./VDataTableServer-B4kmkDp3.js";var O={class:`font-weight-medium`},k={class:`text-body-2`},A={class:`text-caption text-medium-emphasis`},j=g({__name:`AlertsPage`,setup(g){let j=b(),M=f(!1),N=f([]),P=f(0),F=f(1),I=f(null),L=f(null),R=f([{label:`今日告警`,value:`0`,color:`blue`,icon:`mdi-alert`},{label:`活跃告警`,value:`0`,color:`orange`,icon:`mdi-bell-ring`},{label:`已恢复`,value:`0`,color:`green`,icon:`mdi-check-circle`},{label:`Top 服务器`,value:``,color:`red`,icon:`mdi-server`}]),z=[{label:`CPU`,value:`cpu`},{label:`内存`,value:`memory`},{label:`磁盘`,value:`disk`},{label:`连接`,value:`connection`}],B=[{label:`活跃`,value:`active`},{label:`已恢复`,value:`recovered`}],V=[{title:`类型`,key:`alert_type`,width:100},{title:`服务器`,key:`server_name`},{title:`详情`,key:`value`},{title:`状态`,key:`is_recovery`,width:100},{title:`时间`,key:`created_at`,width:160}];async function H(){try{let e=await i.get(`/alert-history/stats`);R.value[0].value=String(e.today||0),R.value[1].value=String(e.active||0),R.value[2].value=String(e.recovered||0),R.value[3].value=e.top_server||``}catch{j(`加载统计失败`,`error`)}}async function U(){M.value=!0;try{let e=await i.get(`/alert-history/`,{page:F.value,per_page:20,type:I.value||void 0,status:L.value||void 0});N.value=e.items||[],P.value=e.total||0}catch{N.value=[]}finally{M.value=!1}}function W(e){return e===`cpu`||e===`CPU`?`error`:e===`memory`||e===`内存`?`warning`:e===`disk`||e===`磁盘`?`info`:`primary`}function G(e){return e===`cpu`||e===`CPU`?`mdi-chip`:e===`memory`||e===`内存`?`mdi-memory`:e===`disk`||e===`磁盘`?`mdi-harddisk`:`mdi-lan-disconnect`}return m(()=>{H(),U()}),(i,f)=>(u(),l(x,{fluid:``,class:`pa-6`},{default:y(()=>[c(C,{class:`mb-4`},{default:y(()=>[(u(!0),h(s,null,_(R.value,e=>(u(),l(S,{cols:`12`,sm:`6`,lg:`3`,key:e.label},{default:y(()=>[c(v,{elevation:`0`,lines:`two`,rounded:`lg`,border:``},{default:y(()=>[c(p,null,{append:y(()=>[c(o,{color:e.color,size:`30`},{default:y(()=>[d(a(e.icon),1)]),_:2},1032,[`color`])]),default:y(()=>[c(t,{class:`text-body-small`},{default:y(()=>[d(a(e.label),1)]),_:2},1024),c(t,null,{default:y(()=>[d(a(e.value),1)]),_:2},1024)]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1}),c(r,{elevation:`0`,border:``,rounded:`lg`},{default:y(()=>[c(n,{class:`d-flex align-center`},{default:y(()=>[f[5]||=d(` 告警历史 `,-1),c(w),c(E,{modelValue:I.value,"onUpdate:modelValue":[f[0]||=e=>I.value=e,f[1]||=e=>{F.value=1,U()}],items:z,"item-title":`label`,"item-value":`value`,label:`类型`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},clearable:``},null,8,[`modelValue`]),c(E,{modelValue:L.value,"onUpdate:modelValue":[f[2]||=e=>L.value=e,f[3]||=e=>{F.value=1,U()}],items:B,"item-title":`label`,"item-value":`value`,label:`状态`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},class:`ml-3`,clearable:``},null,8,[`modelValue`])]),_:1}),c(D,{items:N.value,headers:V,"items-length":P.value,loading:M.value,page:F.value,"items-per-page":20,hover:``,density:`comfortable`,"onUpdate:page":f[4]||=e=>{F.value=e,U()}},{"item.alert_type":y(({item:e})=>[c(T,{color:W(e.alert_type),size:`small`,variant:`tonal`,label:``},{prepend:y(()=>[c(o,{size:`12`},{default:y(()=>[d(a(G(e.alert_type)),1)]),_:2},1024)]),default:y(()=>[d(` `+a(e.alert_type),1)]),_:2},1032,[`color`])]),"item.server_name":y(({item:t})=>[e(`span`,O,a(t.server_name),1)]),"item.value":y(({item:t})=>[e(`span`,k,a(t.value),1)]),"item.is_recovery":y(({item:e})=>[e.is_recovery?(u(),l(T,{key:0,color:`success`,size:`small`,variant:`tonal`,label:``},{default:y(()=>[...f[6]||=[d(`已恢复`,-1)]]),_:1})):(u(),l(T,{key:1,color:`error`,size:`small`,variant:`tonal`,label:``},{default:y(()=>[...f[7]||=[d(`告警中`,-1)]]),_:1}))]),"item.created_at":y(({item:t})=>[e(`span`,A,a(t.created_at),1)]),"no-data":y(()=>[...f[8]||=[e(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`items-length`,`loading`,`page`])]),_:1})]),_:1}))}});export{j as default};
-1
View File
@@ -1 +0,0 @@
import{_ as e,t,v as n}from"./VSelect-BBtNL0eJ.js";import{n as r,t as i}from"./VRow-sbIKY3fI.js";import{A as a,B as o,Cr as s,F as c,Gr as l,Mr as u,N as d,R as f,T as p,Tr as m,Yr as h,dr as g,fr as _,j as v,lr as y,or as b,pr as x,qt as S,rr as C,sr as w,ut as T,xr as E,zr as D}from"./index-DXg_pn7T.js";import{n as O}from"./dataTable-esDY4cOk.js";import{t as k}from"./VDataTableServer-CAOCfrD0.js";var A={class:`font-weight-medium`},j={class:`text-body-2`},M={class:`text-caption text-medium-emphasis`},N=x({__name:`AlertsPage`,setup(x){let N=e(),P=D(!1),F=D([]),I=D(0),L=D(1),R=D(null),z=D(null),B=D([{label:`今日告警`,value:`0`,color:`blue`,icon:`mdi-alert`},{label:`活跃告警`,value:`0`,color:`orange`,icon:`mdi-bell-ring`},{label:`已恢复`,value:`0`,color:`green`,icon:`mdi-check-circle`},{label:`Top 服务器`,value:``,color:`red`,icon:`mdi-server`}]),V=[{label:`CPU`,value:`cpu`},{label:`内存`,value:`memory`},{label:`磁盘`,value:`disk`},{label:`连接`,value:`connection`}],H=[{label:`活跃`,value:`active`},{label:`已恢复`,value:`recovered`}],U=O([{title:`类型`,key:`alert_type`,width:100},{title:`服务器`,key:`server_name`},{title:`详情`,key:`value`},{title:`状态`,key:`is_recovery`,width:100},{title:`时间`,key:`created_at`,width:160}]);async function W(){try{let e=await S.get(`/alert-history/stats`);B.value[0].value=String(e.today||0),B.value[1].value=String(e.active||0),B.value[2].value=String(e.recovered||0),B.value[3].value=e.top_server||``}catch{N(`加载统计失败`,`error`)}}async function G(){P.value=!0;try{let e=await S.get(`/alert-history/`,{page:L.value,per_page:20,type:R.value||void 0,status:z.value||void 0});F.value=e.items||[],I.value=e.total||0}catch{F.value=[]}finally{P.value=!1}}function K(e){return e===`cpu`||e===`CPU`?`error`:e===`memory`||e===`内存`?`warning`:e===`disk`||e===`磁盘`?`info`:`primary`}function q(e){return e===`cpu`||e===`CPU`?`mdi-chip`:e===`memory`||e===`内存`?`mdi-memory`:e===`disk`||e===`磁盘`?`mdi-harddisk`:`mdi-lan-disconnect`}return E(()=>{W(),G()}),(e,x)=>(s(),w(n,{fluid:``,class:`pa-6`},{default:u(()=>[_(i,{class:`mb-4`},{default:u(()=>[(s(!0),y(C,null,m(B.value,e=>(s(),w(r,{cols:`12`,sm:`6`,lg:`3`,key:e.label},{default:u(()=>[_(p,{elevation:`0`,lines:`two`,rounded:`lg`,border:``},{default:u(()=>[_(a,null,{append:u(()=>[_(T,{color:e.color,size:`30`},{default:u(()=>[g(h(e.icon),1)]),_:2},1032,[`color`])]),default:u(()=>[_(v,{class:`text-body-small`},{default:u(()=>[g(h(e.label),1)]),_:2},1024),_(v,null,{default:u(()=>[g(h(e.value),1)]),_:2},1024)]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1}),_(f,{elevation:`0`,border:``,rounded:`lg`},{default:u(()=>[_(o,{class:`d-flex align-center`},{default:u(()=>[x[5]||=g(` 告警历史 `,-1),_(d),_(t,{modelValue:R.value,"onUpdate:modelValue":[x[0]||=e=>R.value=e,x[1]||=e=>{L.value=1,G()}],items:V,"item-title":`label`,"item-value":`value`,label:`类型`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},clearable:``},null,8,[`modelValue`]),_(t,{modelValue:z.value,"onUpdate:modelValue":[x[2]||=e=>z.value=e,x[3]||=e=>{L.value=1,G()}],items:H,"item-title":`label`,"item-value":`value`,label:`状态`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},class:`ml-3`,clearable:``},null,8,[`modelValue`])]),_:1}),_(k,{items:F.value,headers:l(U),"items-length":I.value,loading:P.value,page:L.value,"items-per-page":20,hover:``,density:`comfortable`,"onUpdate:page":x[4]||=e=>{L.value=e,G()}},{"item.alert_type":u(({item:e})=>[_(c,{color:K(e.alert_type),size:`small`,variant:`tonal`,label:``},{prepend:u(()=>[_(T,{size:`12`},{default:u(()=>[g(h(q(e.alert_type)),1)]),_:2},1024)]),default:u(()=>[g(` `+h(e.alert_type),1)]),_:2},1032,[`color`])]),"item.server_name":u(({item:e})=>[b(`span`,A,h(e.server_name),1)]),"item.value":u(({item:e})=>[b(`span`,j,h(e.value),1)]),"item.is_recovery":u(({item:e})=>[e.is_recovery?(s(),w(c,{key:0,color:`success`,size:`small`,variant:`tonal`,label:``},{default:u(()=>[...x[6]||=[g(`已恢复`,-1)]]),_:1})):(s(),w(c,{key:1,color:`error`,size:`small`,variant:`tonal`,label:``},{default:u(()=>[...x[7]||=[g(`告警中`,-1)]]),_:1}))]),"item.created_at":u(({item:e})=>[b(`span`,M,h(e.created_at),1)]),"no-data":u(()=>[...x[8]||=[b(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`headers`,`items-length`,`loading`,`page`])]),_:1})]),_:1}))}});export{N as default};
-1
View File
@@ -1 +0,0 @@
import{Ar as e,It as t,Kn as n,M as r,O as i,P as a,Qn as o,Un as s,Y as c,Yn as l,Zn as u,br as d,dr as f,er as p,gr as m,k as h,lr as g,qn as _,sr as v,w as y}from"./index-BCQTIDw4.js";import{a as b}from"./VSelectionControl-68hkvpet.js";import{n as x,t as S}from"./VRow-C8w9Wz5w.js";import{t as C}from"./VSpacer-CLz5vknL.js";import{t as w}from"./useSnackbar-D7YOfKok.js";import{t as T}from"./VChip-B452RkaL.js";import{t as E}from"./VSelect-D0UpVb40.js";import{t as D}from"./VDataTableServer-B_4ecUrQ.js";var O={class:`font-weight-medium`},k={class:`text-body-2`},A={class:`text-caption text-medium-emphasis`},j=p({__name:`AlertsPage`,setup(p){let j=w(),M=d(!1),N=d([]),P=d(0),F=d(1),I=d(null),L=d(null),R=d([{label:`今日告警`,value:`0`,color:`blue`,icon:`mdi-alert`},{label:`活跃告警`,value:`0`,color:`orange`,icon:`mdi-bell-ring`},{label:`已恢复`,value:`0`,color:`green`,icon:`mdi-check-circle`},{label:`Top 服务器`,value:``,color:`red`,icon:`mdi-server`}]),z=[{label:`CPU`,value:`cpu`},{label:`内存`,value:`memory`},{label:`磁盘`,value:`disk`},{label:`连接`,value:`connection`}],B=[{label:`活跃`,value:`active`},{label:`已恢复`,value:`recovered`}],V=[{title:`类型`,key:`alert_type`,width:100},{title:`服务器`,key:`server_name`},{title:`详情`,key:`value`},{title:`状态`,key:`is_recovery`,width:100},{title:`时间`,key:`created_at`,width:160}];async function H(){try{let e=await t.get(`/alert-history/stats`);R.value[0].value=String(e.today||0),R.value[1].value=String(e.active||0),R.value[2].value=String(e.recovered||0),R.value[3].value=e.top_server||``}catch{j(`加载统计失败`,`error`)}}async function U(){M.value=!0;try{let e=await t.get(`/alert-history/`,{page:F.value,per_page:20,type:I.value||void 0,status:L.value||void 0});N.value=e.items||[],P.value=e.total||0}catch{N.value=[]}finally{M.value=!1}}function W(e){return e===`cpu`||e===`CPU`?`error`:e===`memory`||e===`内存`?`warning`:e===`disk`||e===`磁盘`?`info`:`primary`}function G(e){return e===`cpu`||e===`CPU`?`mdi-chip`:e===`memory`||e===`内存`?`mdi-memory`:e===`disk`||e===`磁盘`?`mdi-harddisk`:`mdi-lan-disconnect`}return v(()=>{H(),U()}),(t,d)=>(g(),_(b,{fluid:``,class:`pa-6`},{default:m(()=>[o(S,{class:`mb-4`},{default:m(()=>[(g(!0),l(s,null,f(R.value,t=>(g(),_(x,{cols:`12`,sm:`6`,lg:`3`,key:t.label},{default:m(()=>[o(y,{elevation:`0`,lines:`two`,rounded:`lg`,border:``},{default:m(()=>[o(i,null,{append:m(()=>[o(c,{color:t.color,size:`30`},{default:m(()=>[u(e(t.icon),1)]),_:2},1032,[`color`])]),default:m(()=>[o(h,{class:`text-body-small`},{default:m(()=>[u(e(t.label),1)]),_:2},1024),o(h,null,{default:m(()=>[u(e(t.value),1)]),_:2},1024)]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1}),o(r,{elevation:`0`,border:``,rounded:`lg`},{default:m(()=>[o(a,{class:`d-flex align-center`},{default:m(()=>[d[5]||=u(` 告警历史 `,-1),o(C),o(E,{modelValue:I.value,"onUpdate:modelValue":[d[0]||=e=>I.value=e,d[1]||=e=>{F.value=1,U()}],items:z,"item-title":`label`,"item-value":`value`,label:`类型`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},clearable:``},null,8,[`modelValue`]),o(E,{modelValue:L.value,"onUpdate:modelValue":[d[2]||=e=>L.value=e,d[3]||=e=>{F.value=1,U()}],items:B,"item-title":`label`,"item-value":`value`,label:`状态`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},class:`ml-3`,clearable:``},null,8,[`modelValue`])]),_:1}),o(D,{items:N.value,headers:V,"items-length":P.value,loading:M.value,page:F.value,"items-per-page":20,hover:``,density:`comfortable`,"onUpdate:page":d[4]||=e=>{F.value=e,U()}},{"item.alert_type":m(({item:t})=>[o(T,{color:W(t.alert_type),size:`small`,variant:`tonal`,label:``},{prepend:m(()=>[o(c,{size:`12`},{default:m(()=>[u(e(G(t.alert_type)),1)]),_:2},1024)]),default:m(()=>[u(` `+e(t.alert_type),1)]),_:2},1032,[`color`])]),"item.server_name":m(({item:t})=>[n(`span`,O,e(t.server_name),1)]),"item.value":m(({item:t})=>[n(`span`,k,e(t.value),1)]),"item.is_recovery":m(({item:e})=>[e.is_recovery?(g(),_(T,{key:0,color:`success`,size:`small`,variant:`tonal`,label:``},{default:m(()=>[...d[6]||=[u(`已恢复`,-1)]]),_:1})):(g(),_(T,{key:1,color:`error`,size:`small`,variant:`tonal`,label:``},{default:m(()=>[...d[7]||=[u(`告警中`,-1)]]),_:1}))]),"item.created_at":m(({item:t})=>[n(`span`,A,e(t.created_at),1)]),"no-data":m(()=>[...d[8]||=[n(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`items-length`,`loading`,`page`])]),_:1})]),_:1}))}});export{j as default};
-1
View File
@@ -1 +0,0 @@
import{_ as e,t,v as n}from"./VSelect-CuoDLDz6.js";import{n as r,t as i}from"./VRow-DoFEriUB.js";import{$r as a,Br as o,Jt as s,Nr as c,X as l,_ as u,_r as d,cr as f,fr as p,h as m,hr as h,jr as g,l as _,m as v,pr as y,qr as b,ri as x,vr as S,y as C,yr as w}from"./index-C-0InWiN.js";import{t as T}from"./usePageActivateRefresh-DvwpdC2e.js";import{n as E}from"./dataTable-esDY4cOk.js";import{_ as D,y as O}from"./VTextField-Dh7LxFTC.js";import{t as k}from"./VDataTableServer-NOoqoVGA.js";var A={class:`font-weight-medium`},j={class:`text-body-2`},M={class:`text-caption text-medium-emphasis`},N=w({name:`AlertsPage`,__name:`AlertsPage`,setup(w){let N=e(),P=b(!1),F=b([]),I=b(0),L=b(1),R=b(null),z=b(null),B=b([{label:`今日告警`,value:`0`,color:`blue`,icon:`mdi-alert`},{label:`活跃告警`,value:`0`,color:`orange`,icon:`mdi-bell-ring`},{label:`已恢复`,value:`0`,color:`green`,icon:`mdi-check-circle`},{label:`Top 服务器`,value:``,color:`red`,icon:`mdi-server`}]),V=[{label:`CPU`,value:`cpu`},{label:`内存`,value:`memory`},{label:`磁盘`,value:`disk`},{label:`连接`,value:`connection`}],H=[{label:`活跃`,value:`active`},{label:`已恢复`,value:`recovered`}],U=E([{title:`类型`,key:`alert_type`,width:100},{title:`服务器`,key:`server_name`},{title:`详情`,key:`value`},{title:`状态`,key:`is_recovery`,width:100},{title:`时间`,key:`created_at`,width:160}]);async function W(){try{let e=await s.get(`/alert-history/stats`);B.value[0].value=String(e.today||0),B.value[1].value=String(e.active||0),B.value[2].value=String(e.recovered||0),B.value[3].value=e.top_server||``}catch{N(`加载统计失败`,`error`)}}async function G(e=!1){e||(P.value=!0);try{let e=await s.get(`/alert-history/`,{page:L.value,per_page:20,type:R.value||void 0,status:z.value||void 0});F.value=e.items||[],I.value=e.total||0}catch{F.value=[]}finally{e||(P.value=!1)}}function K(e){return e===`cpu`||e===`CPU`?`error`:e===`memory`||e===`内存`?`warning`:e===`disk`||e===`磁盘`?`info`:`primary`}function q(e){return e===`cpu`||e===`CPU`?`mdi-chip`:e===`memory`||e===`内存`?`mdi-memory`:e===`disk`||e===`磁盘`?`mdi-harddisk`:`mdi-lan-disconnect`}async function J(e=!1){await Promise.all([W(),G(e)])}return T(e=>J(e)),(e,s)=>(g(),y(n,{fluid:``,class:`pa-6`},{default:o(()=>[S(i,{class:`mb-4`},{default:o(()=>[(g(!0),h(f,null,c(B.value,e=>(g(),y(r,{cols:`12`,sm:`6`,lg:`3`,key:e.label},{default:o(()=>[S(_,{elevation:`0`,lines:`two`,rounded:`lg`,border:``},{default:o(()=>[S(v,null,{append:o(()=>[S(l,{color:e.color,size:`30`},{default:o(()=>[d(x(e.icon),1)]),_:2},1032,[`color`])]),default:o(()=>[S(m,{class:`text-body-small`},{default:o(()=>[d(x(e.label),1)]),_:2},1024),S(m,null,{default:o(()=>[d(x(e.value),1)]),_:2},1024)]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1}),S(D,{elevation:`0`,border:``,rounded:`lg`},{default:o(()=>[S(O,{class:`d-flex align-center`},{default:o(()=>[s[5]||=d(` 告警历史 `,-1),S(u),S(t,{modelValue:R.value,"onUpdate:modelValue":[s[0]||=e=>R.value=e,s[1]||=e=>{L.value=1,G()}],items:V,"item-title":`label`,"item-value":`value`,label:`类型`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},clearable:``},null,8,[`modelValue`]),S(t,{modelValue:z.value,"onUpdate:modelValue":[s[2]||=e=>z.value=e,s[3]||=e=>{L.value=1,G()}],items:H,"item-title":`label`,"item-value":`value`,label:`状态`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},class:`ml-3`,clearable:``},null,8,[`modelValue`])]),_:1}),S(k,{items:F.value,headers:a(U),"items-length":I.value,loading:P.value,page:L.value,"items-per-page":20,hover:``,density:`comfortable`,"onUpdate:page":s[4]||=e=>{L.value=e,G()}},{"item.alert_type":o(({item:e})=>[S(C,{color:K(e.alert_type),size:`small`,variant:`tonal`,label:``},{prepend:o(()=>[S(l,{size:`12`},{default:o(()=>[d(x(q(e.alert_type)),1)]),_:2},1024)]),default:o(()=>[d(` `+x(e.alert_type),1)]),_:2},1032,[`color`])]),"item.server_name":o(({item:e})=>[p(`span`,A,x(e.server_name),1)]),"item.value":o(({item:e})=>[p(`span`,j,x(e.value),1)]),"item.is_recovery":o(({item:e})=>[e.is_recovery?(g(),y(C,{key:0,color:`success`,size:`small`,variant:`tonal`,label:``},{default:o(()=>[...s[6]||=[d(`已恢复`,-1)]]),_:1})):(g(),y(C,{key:1,color:`error`,size:`small`,variant:`tonal`,label:``},{default:o(()=>[...s[7]||=[d(`告警中`,-1)]]),_:1}))]),"item.created_at":o(({item:e})=>[p(`span`,M,x(e.created_at),1)]),"no-data":o(()=>[...s[8]||=[p(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`headers`,`items-length`,`loading`,`page`])]),_:1})]),_:1}))}});export{N as default};
-1
View File
@@ -1 +0,0 @@
import{Dr as e,Kn as t,Lr as n,Lt as r,M as i,O as a,P as o,Qn as s,Xn as c,Y as l,Yn as u,br as d,er as f,fr as p,k as m,mr as h,rr as g,tr as _,ur as v,w as y}from"./index-DlQ7NONs.js";import{a as b}from"./VSelectionControl-DzIMhLnk.js";import{n as x,t as S}from"./VRow-ClkRhHW4.js";import{t as C}from"./VSpacer-CKw4fJfQ.js";import{t as w}from"./useSnackbar-D7YOfKok.js";import{t as T}from"./VChip-CqKVMdU4.js";import{t as E}from"./VSelect-CA1qJw1j.js";import{t as D}from"./VDataTableServer-BdF5Y6fh.js";var O={class:`font-weight-medium`},k={class:`text-body-2`},A={class:`text-caption text-medium-emphasis`},j=g({__name:`AlertsPage`,setup(g){let j=w(),M=e(!1),N=e([]),P=e(0),F=e(1),I=e(null),L=e(null),R=e([{label:`今日告警`,value:`0`,color:`blue`,icon:`mdi-alert`},{label:`活跃告警`,value:`0`,color:`orange`,icon:`mdi-bell-ring`},{label:`已恢复`,value:`0`,color:`green`,icon:`mdi-check-circle`},{label:`Top 服务器`,value:``,color:`red`,icon:`mdi-server`}]),z=[{label:`CPU`,value:`cpu`},{label:`内存`,value:`memory`},{label:`磁盘`,value:`disk`},{label:`连接`,value:`connection`}],B=[{label:`活跃`,value:`active`},{label:`已恢复`,value:`recovered`}],V=[{title:`类型`,key:`alert_type`,width:100},{title:`服务器`,key:`server_name`},{title:`详情`,key:`value`},{title:`状态`,key:`is_recovery`,width:100},{title:`时间`,key:`created_at`,width:160}];async function H(){try{let e=await r.get(`/alert-history/stats`);R.value[0].value=String(e.today||0),R.value[1].value=String(e.active||0),R.value[2].value=String(e.recovered||0),R.value[3].value=e.top_server||``}catch{j(`加载统计失败`,`error`)}}async function U(){M.value=!0;try{let e=await r.get(`/alert-history/`,{page:F.value,per_page:20,type:I.value||void 0,status:L.value||void 0});N.value=e.items||[],P.value=e.total||0}catch{N.value=[]}finally{M.value=!1}}function W(e){return e===`cpu`||e===`CPU`?`error`:e===`memory`||e===`内存`?`warning`:e===`disk`||e===`磁盘`?`info`:`primary`}function G(e){return e===`cpu`||e===`CPU`?`mdi-chip`:e===`memory`||e===`内存`?`mdi-memory`:e===`disk`||e===`磁盘`?`mdi-harddisk`:`mdi-lan-disconnect`}return v(()=>{H(),U()}),(e,r)=>(p(),c(b,{fluid:``,class:`pa-6`},{default:d(()=>[_(S,{class:`mb-4`},{default:d(()=>[(p(!0),s(t,null,h(R.value,e=>(p(),c(x,{cols:`12`,sm:`6`,lg:`3`,key:e.label},{default:d(()=>[_(y,{elevation:`0`,lines:`two`,rounded:`lg`,border:``},{default:d(()=>[_(a,null,{append:d(()=>[_(l,{color:e.color,size:`30`},{default:d(()=>[f(n(e.icon),1)]),_:2},1032,[`color`])]),default:d(()=>[_(m,{class:`text-body-small`},{default:d(()=>[f(n(e.label),1)]),_:2},1024),_(m,null,{default:d(()=>[f(n(e.value),1)]),_:2},1024)]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1}),_(i,{elevation:`0`,border:``,rounded:`lg`},{default:d(()=>[_(o,{class:`d-flex align-center`},{default:d(()=>[r[5]||=f(` 告警历史 `,-1),_(C),_(E,{modelValue:I.value,"onUpdate:modelValue":[r[0]||=e=>I.value=e,r[1]||=e=>{F.value=1,U()}],items:z,"item-title":`label`,"item-value":`value`,label:`类型`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},clearable:``},null,8,[`modelValue`]),_(E,{modelValue:L.value,"onUpdate:modelValue":[r[2]||=e=>L.value=e,r[3]||=e=>{F.value=1,U()}],items:B,"item-title":`label`,"item-value":`value`,label:`状态`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},class:`ml-3`,clearable:``},null,8,[`modelValue`])]),_:1}),_(D,{items:N.value,headers:V,"items-length":P.value,loading:M.value,page:F.value,"items-per-page":20,hover:``,density:`comfortable`,"onUpdate:page":r[4]||=e=>{F.value=e,U()}},{"item.alert_type":d(({item:e})=>[_(T,{color:W(e.alert_type),size:`small`,variant:`tonal`,label:``},{prepend:d(()=>[_(l,{size:`12`},{default:d(()=>[f(n(G(e.alert_type)),1)]),_:2},1024)]),default:d(()=>[f(` `+n(e.alert_type),1)]),_:2},1032,[`color`])]),"item.server_name":d(({item:e})=>[u(`span`,O,n(e.server_name),1)]),"item.value":d(({item:e})=>[u(`span`,k,n(e.value),1)]),"item.is_recovery":d(({item:e})=>[e.is_recovery?(p(),c(T,{key:0,color:`success`,size:`small`,variant:`tonal`,label:``},{default:d(()=>[...r[6]||=[f(`已恢复`,-1)]]),_:1})):(p(),c(T,{key:1,color:`error`,size:`small`,variant:`tonal`,label:``},{default:d(()=>[...r[7]||=[f(`告警中`,-1)]]),_:1}))]),"item.created_at":d(({item:e})=>[u(`span`,A,n(e.created_at),1)]),"no-data":d(()=>[...r[8]||=[u(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`items-length`,`loading`,`page`])]),_:1})]),_:1}))}});export{j as default};
-1
View File
@@ -1 +0,0 @@
import{_ as e,t,v as n}from"./VSelect-VZwjQp1g.js";import{n as r,t as i}from"./VRow-BTwQ8TgI.js";import{Er as a,Fr as o,Hr as s,Ht as c,Jr as l,Or as u,Qr as d,X as f,_ as p,cr as m,fr as h,h as g,l as _,m as v,mr as y,pr as b,rr as x,sr as S,ur as C,y as w}from"./index-C5G-svaZ.js";import{t as T}from"./usePageActivateRefresh-_GI9FPxk.js";import{n as E}from"./dataTable-esDY4cOk.js";import{_ as D,y as O}from"./VTextField-BL2GiCZc.js";import{t as k}from"./VDataTableServer-C9jowZgk.js";var A={class:`font-weight-medium`},j={class:`text-body-2`},M={class:`text-caption text-medium-emphasis`},N=y({name:`AlertsPage`,__name:`AlertsPage`,setup(y){let N=e(),P=s(!1),F=s([]),I=s(0),L=s(1),R=s(null),z=s(null),B=s([{label:`今日告警`,value:`0`,color:`blue`,icon:`mdi-alert`},{label:`活跃告警`,value:`0`,color:`orange`,icon:`mdi-bell-ring`},{label:`已恢复`,value:`0`,color:`green`,icon:`mdi-check-circle`},{label:`Top 服务器`,value:``,color:`red`,icon:`mdi-server`}]),V=[{label:`CPU`,value:`cpu`},{label:`内存`,value:`memory`},{label:`磁盘`,value:`disk`},{label:`连接`,value:`connection`}],H=[{label:`活跃`,value:`active`},{label:`已恢复`,value:`recovered`}],U=E([{title:`类型`,key:`alert_type`,width:100},{title:`服务器`,key:`server_name`},{title:`详情`,key:`value`},{title:`状态`,key:`is_recovery`,width:100},{title:`时间`,key:`created_at`,width:160}]);async function W(){try{let e=await c.get(`/alert-history/stats`);B.value[0].value=String(e.today||0),B.value[1].value=String(e.active||0),B.value[2].value=String(e.recovered||0),B.value[3].value=e.top_server||``}catch{N(`加载统计失败`,`error`)}}async function G(e=!1){e||(P.value=!0);try{let e=await c.get(`/alert-history/`,{page:L.value,per_page:20,type:R.value||void 0,status:z.value||void 0});F.value=e.items||[],I.value=e.total||0}catch{F.value=[]}finally{e||(P.value=!1)}}function K(e){return e===`cpu`||e===`CPU`?`error`:e===`memory`||e===`内存`?`warning`:e===`disk`||e===`磁盘`?`info`:`primary`}function q(e){return e===`cpu`||e===`CPU`?`mdi-chip`:e===`memory`||e===`内存`?`mdi-memory`:e===`disk`||e===`磁盘`?`mdi-harddisk`:`mdi-lan-disconnect`}async function J(e=!1){await Promise.all([W(),G(e)])}return T(e=>J(e)),(e,s)=>(a(),m(n,{fluid:``,class:`pa-6`},{default:o(()=>[b(i,{class:`mb-4`},{default:o(()=>[(a(!0),C(x,null,u(B.value,e=>(a(),m(r,{cols:`12`,sm:`6`,lg:`3`,key:e.label},{default:o(()=>[b(_,{elevation:`0`,lines:`two`,rounded:`lg`,border:``},{default:o(()=>[b(v,null,{append:o(()=>[b(f,{color:e.color,size:`30`},{default:o(()=>[h(d(e.icon),1)]),_:2},1032,[`color`])]),default:o(()=>[b(g,{class:`text-body-small`},{default:o(()=>[h(d(e.label),1)]),_:2},1024),b(g,null,{default:o(()=>[h(d(e.value),1)]),_:2},1024)]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1}),b(D,{elevation:`0`,border:``,rounded:`lg`},{default:o(()=>[b(O,{class:`d-flex align-center`},{default:o(()=>[s[5]||=h(` 告警历史 `,-1),b(p),b(t,{modelValue:R.value,"onUpdate:modelValue":[s[0]||=e=>R.value=e,s[1]||=e=>{L.value=1,G()}],items:V,"item-title":`label`,"item-value":`value`,label:`类型`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},clearable:``},null,8,[`modelValue`]),b(t,{modelValue:z.value,"onUpdate:modelValue":[s[2]||=e=>z.value=e,s[3]||=e=>{L.value=1,G()}],items:H,"item-title":`label`,"item-value":`value`,label:`状态`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},class:`ml-3`,clearable:``},null,8,[`modelValue`])]),_:1}),b(k,{items:F.value,headers:l(U),"items-length":I.value,loading:P.value,page:L.value,"items-per-page":20,hover:``,density:`comfortable`,"onUpdate:page":s[4]||=e=>{L.value=e,G()}},{"item.alert_type":o(({item:e})=>[b(w,{color:K(e.alert_type),size:`small`,variant:`tonal`,label:``},{prepend:o(()=>[b(f,{size:`12`},{default:o(()=>[h(d(q(e.alert_type)),1)]),_:2},1024)]),default:o(()=>[h(` `+d(e.alert_type),1)]),_:2},1032,[`color`])]),"item.server_name":o(({item:e})=>[S(`span`,A,d(e.server_name),1)]),"item.value":o(({item:e})=>[S(`span`,j,d(e.value),1)]),"item.is_recovery":o(({item:e})=>[e.is_recovery?(a(),m(w,{key:0,color:`success`,size:`small`,variant:`tonal`,label:``},{default:o(()=>[...s[6]||=[h(`已恢复`,-1)]]),_:1})):(a(),m(w,{key:1,color:`error`,size:`small`,variant:`tonal`,label:``},{default:o(()=>[...s[7]||=[h(`告警中`,-1)]]),_:1}))]),"item.created_at":o(({item:e})=>[S(`span`,M,d(e.created_at),1)]),"no-data":o(()=>[...s[8]||=[S(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`headers`,`items-length`,`loading`,`page`])]),_:1})]),_:1}))}});export{N as default};
-1
View File
@@ -1 +0,0 @@
import{$n as e,A as t,Ar as n,Br as r,Cr as i,F as a,N as o,Rt as s,X as c,Xn as l,_r as u,ar as d,er as f,hr as p,ir as m,k as h,nr as g,or as _,pr as v,w as y}from"./index-o0-2k-vI.js";import{o as b,t as x}from"./VContainer-BbTGUkA2.js";import{t as S}from"./VChip-B_7LyIUT.js";import{t as C}from"./VSelect-RidH-jin.js";import{t as w}from"./VDataTableServer-CYEpP3ht.js";import{n as T,t as E}from"./VRow-D9pCZkmz.js";import{t as D}from"./VSpacer-DE-o2Lgp.js";var O={class:`font-weight-medium`},k={class:`text-body-2`},A={class:`text-caption text-medium-emphasis`},j=_({__name:`AlertsPage`,setup(_){let j=b(),M=n(!1),N=n([]),P=n(0),F=n(1),I=n(null),L=n(null),R=n([{label:`今日告警`,value:`0`,color:`blue`,icon:`mdi-alert`},{label:`活跃告警`,value:`0`,color:`orange`,icon:`mdi-bell-ring`},{label:`已恢复`,value:`0`,color:`green`,icon:`mdi-check-circle`},{label:`Top 服务器`,value:``,color:`red`,icon:`mdi-server`}]),z=[{label:`CPU`,value:`cpu`},{label:`内存`,value:`memory`},{label:`磁盘`,value:`disk`},{label:`连接`,value:`connection`}],B=[{label:`活跃`,value:`active`},{label:`已恢复`,value:`recovered`}],V=[{title:`类型`,key:`alert_type`,width:100},{title:`服务器`,key:`server_name`},{title:`详情`,key:`value`},{title:`状态`,key:`is_recovery`,width:100},{title:`时间`,key:`created_at`,width:160}];async function H(){try{let e=await s.get(`/alert-history/stats`);R.value[0].value=String(e.today||0),R.value[1].value=String(e.active||0),R.value[2].value=String(e.recovered||0),R.value[3].value=e.top_server||``}catch{j(`加载统计失败`,`error`)}}async function U(){M.value=!0;try{let e=await s.get(`/alert-history/`,{page:F.value,per_page:20,type:I.value||void 0,status:L.value||void 0});N.value=e.items||[],P.value=e.total||0}catch{N.value=[]}finally{M.value=!1}}function W(e){return e===`cpu`||e===`CPU`?`error`:e===`memory`||e===`内存`?`warning`:e===`disk`||e===`磁盘`?`info`:`primary`}function G(e){return e===`cpu`||e===`CPU`?`mdi-chip`:e===`memory`||e===`内存`?`mdi-memory`:e===`disk`||e===`磁盘`?`mdi-harddisk`:`mdi-lan-disconnect`}return v(()=>{H(),U()}),(n,s)=>(p(),f(x,{fluid:``,class:`pa-6`},{default:i(()=>[d(E,{class:`mb-4`},{default:i(()=>[(p(!0),g(l,null,u(R.value,e=>(p(),f(T,{cols:`12`,sm:`6`,lg:`3`,key:e.label},{default:i(()=>[d(y,{elevation:`0`,lines:`two`,rounded:`lg`,border:``},{default:i(()=>[d(h,null,{append:i(()=>[d(c,{color:e.color,size:`30`},{default:i(()=>[m(r(e.icon),1)]),_:2},1032,[`color`])]),default:i(()=>[d(t,{class:`text-body-small`},{default:i(()=>[m(r(e.label),1)]),_:2},1024),d(t,null,{default:i(()=>[m(r(e.value),1)]),_:2},1024)]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1}),d(o,{elevation:`0`,border:``,rounded:`lg`},{default:i(()=>[d(a,{class:`d-flex align-center`},{default:i(()=>[s[5]||=m(` 告警历史 `,-1),d(D),d(C,{modelValue:I.value,"onUpdate:modelValue":[s[0]||=e=>I.value=e,s[1]||=e=>{F.value=1,U()}],items:z,"item-title":`label`,"item-value":`value`,label:`类型`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},clearable:``},null,8,[`modelValue`]),d(C,{modelValue:L.value,"onUpdate:modelValue":[s[2]||=e=>L.value=e,s[3]||=e=>{F.value=1,U()}],items:B,"item-title":`label`,"item-value":`value`,label:`状态`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},class:`ml-3`,clearable:``},null,8,[`modelValue`])]),_:1}),d(w,{items:N.value,headers:V,"items-length":P.value,loading:M.value,page:F.value,"items-per-page":20,hover:``,density:`comfortable`,"onUpdate:page":s[4]||=e=>{F.value=e,U()}},{"item.alert_type":i(({item:e})=>[d(S,{color:W(e.alert_type),size:`small`,variant:`tonal`,label:``},{prepend:i(()=>[d(c,{size:`12`},{default:i(()=>[m(r(G(e.alert_type)),1)]),_:2},1024)]),default:i(()=>[m(` `+r(e.alert_type),1)]),_:2},1032,[`color`])]),"item.server_name":i(({item:t})=>[e(`span`,O,r(t.server_name),1)]),"item.value":i(({item:t})=>[e(`span`,k,r(t.value),1)]),"item.is_recovery":i(({item:e})=>[e.is_recovery?(p(),f(S,{key:0,color:`success`,size:`small`,variant:`tonal`,label:``},{default:i(()=>[...s[6]||=[m(`已恢复`,-1)]]),_:1})):(p(),f(S,{key:1,color:`error`,size:`small`,variant:`tonal`,label:``},{default:i(()=>[...s[7]||=[m(`告警中`,-1)]]),_:1}))]),"item.created_at":i(({item:t})=>[e(`span`,A,r(t.created_at),1)]),"no-data":i(()=>[...s[8]||=[e(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`items-length`,`loading`,`page`])]),_:1})]),_:1}))}});export{j as default};
-1
View File
@@ -1 +0,0 @@
import{$n as e,A as t,Jn as n,Lt as r,M as i,P as a,Qn as o,T as s,Wn as c,Xn as l,Y as u,_r as d,cr as f,fr as p,jr as m,k as h,qn as g,tr as _,ur as v,xr as y}from"./index-B44VVsv4.js";import{a as b}from"./VSelectionControl-MVSTnl4E.js";import{n as x,t as S}from"./VRow-D32AkCr-.js";import{t as C}from"./VSpacer-v09_iay_.js";import{t as w}from"./useSnackbar-D7YOfKok.js";import{t as T}from"./VChip-DgyCgFSY.js";import{t as E}from"./VSelect-CBcj5X0U.js";import{t as D}from"./VDataTableServer-cag3KzUN.js";var O={class:`font-weight-medium`},k={class:`text-body-2`},A={class:`text-caption text-medium-emphasis`},j=_({__name:`AlertsPage`,setup(_){let j=w(),M=y(!1),N=y([]),P=y(0),F=y(1),I=y(null),L=y(null),R=y([{label:`今日告警`,value:`0`,color:`blue`,icon:`mdi-alert`},{label:`活跃告警`,value:`0`,color:`orange`,icon:`mdi-bell-ring`},{label:`已恢复`,value:`0`,color:`green`,icon:`mdi-check-circle`},{label:`Top 服务器`,value:``,color:`red`,icon:`mdi-server`}]),z=[{label:`CPU`,value:`cpu`},{label:`内存`,value:`memory`},{label:`磁盘`,value:`disk`},{label:`连接`,value:`connection`}],B=[{label:`活跃`,value:`active`},{label:`已恢复`,value:`recovered`}],V=[{title:`类型`,key:`alert_type`,width:100},{title:`服务器`,key:`server_name`},{title:`详情`,key:`value`},{title:`状态`,key:`is_recovery`,width:100},{title:`时间`,key:`created_at`,width:160}];async function H(){try{let e=await r.get(`/alert-history/stats`);R.value[0].value=String(e.today||0),R.value[1].value=String(e.active||0),R.value[2].value=String(e.recovered||0),R.value[3].value=e.top_server||``}catch{j(`加载统计失败`,`error`)}}async function U(){M.value=!0;try{let e=await r.get(`/alert-history/`,{page:F.value,per_page:20,type:I.value||void 0,status:L.value||void 0});N.value=e.items||[],P.value=e.total||0}catch{N.value=[]}finally{M.value=!1}}function W(e){return e===`cpu`||e===`CPU`?`error`:e===`memory`||e===`内存`?`warning`:e===`disk`||e===`磁盘`?`info`:`primary`}function G(e){return e===`cpu`||e===`CPU`?`mdi-chip`:e===`memory`||e===`内存`?`mdi-memory`:e===`disk`||e===`磁盘`?`mdi-harddisk`:`mdi-lan-disconnect`}return f(()=>{H(),U()}),(r,f)=>(v(),n(b,{fluid:``,class:`pa-6`},{default:d(()=>[e(S,{class:`mb-4`},{default:d(()=>[(v(!0),l(c,null,p(R.value,r=>(v(),n(x,{cols:`12`,sm:`6`,lg:`3`,key:r.label},{default:d(()=>[e(s,{elevation:`0`,lines:`two`,rounded:`lg`,border:``},{default:d(()=>[e(h,null,{append:d(()=>[e(u,{color:r.color,size:`30`},{default:d(()=>[o(m(r.icon),1)]),_:2},1032,[`color`])]),default:d(()=>[e(t,{class:`text-body-small`},{default:d(()=>[o(m(r.label),1)]),_:2},1024),e(t,null,{default:d(()=>[o(m(r.value),1)]),_:2},1024)]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1}),e(i,{elevation:`0`,border:``,rounded:`lg`},{default:d(()=>[e(a,{class:`d-flex align-center`},{default:d(()=>[f[5]||=o(` 告警历史 `,-1),e(C),e(E,{modelValue:I.value,"onUpdate:modelValue":[f[0]||=e=>I.value=e,f[1]||=e=>{F.value=1,U()}],items:z,"item-title":`label`,"item-value":`value`,label:`类型`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},clearable:``},null,8,[`modelValue`]),e(E,{modelValue:L.value,"onUpdate:modelValue":[f[2]||=e=>L.value=e,f[3]||=e=>{F.value=1,U()}],items:B,"item-title":`label`,"item-value":`value`,label:`状态`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},class:`ml-3`,clearable:``},null,8,[`modelValue`])]),_:1}),e(D,{items:N.value,headers:V,"items-length":P.value,loading:M.value,page:F.value,"items-per-page":20,hover:``,density:`comfortable`,"onUpdate:page":f[4]||=e=>{F.value=e,U()}},{"item.alert_type":d(({item:t})=>[e(T,{color:W(t.alert_type),size:`small`,variant:`tonal`,label:``},{prepend:d(()=>[e(u,{size:`12`},{default:d(()=>[o(m(G(t.alert_type)),1)]),_:2},1024)]),default:d(()=>[o(` `+m(t.alert_type),1)]),_:2},1032,[`color`])]),"item.server_name":d(({item:e})=>[g(`span`,O,m(e.server_name),1)]),"item.value":d(({item:e})=>[g(`span`,k,m(e.value),1)]),"item.is_recovery":d(({item:e})=>[e.is_recovery?(v(),n(T,{key:0,color:`success`,size:`small`,variant:`tonal`,label:``},{default:d(()=>[...f[6]||=[o(`已恢复`,-1)]]),_:1})):(v(),n(T,{key:1,color:`error`,size:`small`,variant:`tonal`,label:``},{default:d(()=>[...f[7]||=[o(`告警中`,-1)]]),_:1}))]),"item.created_at":d(({item:e})=>[g(`span`,A,m(e.created_at),1)]),"no-data":d(()=>[...f[8]||=[g(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`items-length`,`loading`,`page`])]),_:1})]),_:1}))}});export{j as default};
-1
View File
@@ -1 +0,0 @@
import{Fr as e,Kn as t,Lt as n,M as r,O as i,P as a,Qn as o,Tr as s,Xn as c,Y as l,Yn as u,br as d,er as f,fr as p,k as m,mr as h,rr as g,tr as _,ur as v,w as y}from"./index-BK7IqYFO.js";import{a as b}from"./VSelectionControl-DfqKEOBG.js";import{n as x,t as S}from"./VRow-CunxP72r.js";import{t as C}from"./VSpacer-iEyUcuqg.js";import{t as w}from"./useSnackbar-D7YOfKok.js";import{t as T}from"./VChip-BiQSOiSv.js";import{t as E}from"./VSelect-DUDHUuZi.js";import{t as D}from"./VDataTableServer-CWRB8rfU.js";var O={class:`font-weight-medium`},k={class:`text-body-2`},A={class:`text-caption text-medium-emphasis`},j=g({__name:`AlertsPage`,setup(g){let j=w(),M=s(!1),N=s([]),P=s(0),F=s(1),I=s(null),L=s(null),R=s([{label:`今日告警`,value:`0`,color:`blue`,icon:`mdi-alert`},{label:`活跃告警`,value:`0`,color:`orange`,icon:`mdi-bell-ring`},{label:`已恢复`,value:`0`,color:`green`,icon:`mdi-check-circle`},{label:`Top 服务器`,value:``,color:`red`,icon:`mdi-server`}]),z=[{label:`CPU`,value:`cpu`},{label:`内存`,value:`memory`},{label:`磁盘`,value:`disk`},{label:`连接`,value:`connection`}],B=[{label:`活跃`,value:`active`},{label:`已恢复`,value:`recovered`}],V=[{title:`类型`,key:`alert_type`,width:100},{title:`服务器`,key:`server_name`},{title:`详情`,key:`value`},{title:`状态`,key:`is_recovery`,width:100},{title:`时间`,key:`created_at`,width:160}];async function H(){try{let e=await n.get(`/alert-history/stats`);R.value[0].value=String(e.today||0),R.value[1].value=String(e.active||0),R.value[2].value=String(e.recovered||0),R.value[3].value=e.top_server||``}catch{j(`加载统计失败`,`error`)}}async function U(){M.value=!0;try{let e=await n.get(`/alert-history/`,{page:F.value,per_page:20,type:I.value||void 0,status:L.value||void 0});N.value=e.items||[],P.value=e.total||0}catch{N.value=[]}finally{M.value=!1}}function W(e){return e===`cpu`||e===`CPU`?`error`:e===`memory`||e===`内存`?`warning`:e===`disk`||e===`磁盘`?`info`:`primary`}function G(e){return e===`cpu`||e===`CPU`?`mdi-chip`:e===`memory`||e===`内存`?`mdi-memory`:e===`disk`||e===`磁盘`?`mdi-harddisk`:`mdi-lan-disconnect`}return v(()=>{H(),U()}),(n,s)=>(p(),c(b,{fluid:``,class:`pa-6`},{default:d(()=>[_(S,{class:`mb-4`},{default:d(()=>[(p(!0),o(t,null,h(R.value,t=>(p(),c(x,{cols:`12`,sm:`6`,lg:`3`,key:t.label},{default:d(()=>[_(y,{elevation:`0`,lines:`two`,rounded:`lg`,border:``},{default:d(()=>[_(i,null,{append:d(()=>[_(l,{color:t.color,size:`30`},{default:d(()=>[f(e(t.icon),1)]),_:2},1032,[`color`])]),default:d(()=>[_(m,{class:`text-body-small`},{default:d(()=>[f(e(t.label),1)]),_:2},1024),_(m,null,{default:d(()=>[f(e(t.value),1)]),_:2},1024)]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1}),_(r,{elevation:`0`,border:``,rounded:`lg`},{default:d(()=>[_(a,{class:`d-flex align-center`},{default:d(()=>[s[5]||=f(` 告警历史 `,-1),_(C),_(E,{modelValue:I.value,"onUpdate:modelValue":[s[0]||=e=>I.value=e,s[1]||=e=>{F.value=1,U()}],items:z,"item-title":`label`,"item-value":`value`,label:`类型`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},clearable:``},null,8,[`modelValue`]),_(E,{modelValue:L.value,"onUpdate:modelValue":[s[2]||=e=>L.value=e,s[3]||=e=>{F.value=1,U()}],items:B,"item-title":`label`,"item-value":`value`,label:`状态`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},class:`ml-3`,clearable:``},null,8,[`modelValue`])]),_:1}),_(D,{items:N.value,headers:V,"items-length":P.value,loading:M.value,page:F.value,"items-per-page":20,hover:``,density:`comfortable`,"onUpdate:page":s[4]||=e=>{F.value=e,U()}},{"item.alert_type":d(({item:t})=>[_(T,{color:W(t.alert_type),size:`small`,variant:`tonal`,label:``},{prepend:d(()=>[_(l,{size:`12`},{default:d(()=>[f(e(G(t.alert_type)),1)]),_:2},1024)]),default:d(()=>[f(` `+e(t.alert_type),1)]),_:2},1032,[`color`])]),"item.server_name":d(({item:t})=>[u(`span`,O,e(t.server_name),1)]),"item.value":d(({item:t})=>[u(`span`,k,e(t.value),1)]),"item.is_recovery":d(({item:e})=>[e.is_recovery?(p(),c(T,{key:0,color:`success`,size:`small`,variant:`tonal`,label:``},{default:d(()=>[...s[6]||=[f(`已恢复`,-1)]]),_:1})):(p(),c(T,{key:1,color:`error`,size:`small`,variant:`tonal`,label:``},{default:d(()=>[...s[7]||=[f(`告警中`,-1)]]),_:1}))]),"item.created_at":d(({item:t})=>[u(`span`,A,e(t.created_at),1)]),"no-data":d(()=>[...s[8]||=[u(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`items-length`,`loading`,`page`])]),_:1})]),_:1}))}});export{j as default};
-1
View File
@@ -1 +0,0 @@
import{$n as e,A as t,Gn as n,Jn as r,M as i,P as a,Qn as o,Rt as s,T as c,Xn as l,Y as u,_r as d,cr as f,fr as p,jr as m,k as h,qn as g,tr as _,ur as v,xr as y}from"./index-C5ry6YBg.js";import{t as b}from"./useSnackbar-D7YOfKok.js";import{t as x}from"./VChip-Bp2qyHYR.js";import{a as S}from"./VSelectionControl-DfREn57y.js";import{n as C,t as w}from"./VRow-dNmEpatS.js";import{t as T}from"./VSpacer-B-v5PorU.js";import{t as E}from"./VSelect-C3osRHZB.js";import{t as D}from"./VDataTableServer-CAVuZImV.js";var O={class:`font-weight-medium`},k={class:`text-body-2`},A={class:`text-caption text-medium-emphasis`},j=_({__name:`AlertsPage`,setup(_){let j=b(),M=y(!1),N=y([]),P=y(0),F=y(1),I=y(null),L=y(null),R=y([{label:`今日告警`,value:`0`,color:`blue`,icon:`mdi-alert`},{label:`活跃告警`,value:`0`,color:`orange`,icon:`mdi-bell-ring`},{label:`已恢复`,value:`0`,color:`green`,icon:`mdi-check-circle`},{label:`Top 服务器`,value:``,color:`red`,icon:`mdi-server`}]),z=[{label:`CPU`,value:`cpu`},{label:`内存`,value:`memory`},{label:`磁盘`,value:`disk`},{label:`连接`,value:`connection`}],B=[{label:`活跃`,value:`active`},{label:`已恢复`,value:`recovered`}],V=[{title:`类型`,key:`alert_type`,width:100},{title:`服务器`,key:`server_name`},{title:`详情`,key:`value`},{title:`状态`,key:`is_recovery`,width:100},{title:`时间`,key:`created_at`,width:160}];async function H(){try{let e=await s.get(`/alert-history/stats`);R.value[0].value=String(e.today||0),R.value[1].value=String(e.active||0),R.value[2].value=String(e.recovered||0),R.value[3].value=e.top_server||``}catch{j(`加载统计失败`,`error`)}}async function U(){M.value=!0;try{let e=await s.get(`/alert-history/`,{page:F.value,per_page:20,type:I.value||void 0,status:L.value||void 0});N.value=e.items||[],P.value=e.total||0}catch{N.value=[]}finally{M.value=!1}}function W(e){return e===`cpu`||e===`CPU`?`error`:e===`memory`||e===`内存`?`warning`:e===`disk`||e===`磁盘`?`info`:`primary`}function G(e){return e===`cpu`||e===`CPU`?`mdi-chip`:e===`memory`||e===`内存`?`mdi-memory`:e===`disk`||e===`磁盘`?`mdi-harddisk`:`mdi-lan-disconnect`}return f(()=>{H(),U()}),(s,f)=>(v(),r(S,{fluid:``,class:`pa-6`},{default:d(()=>[e(w,{class:`mb-4`},{default:d(()=>[(v(!0),l(n,null,p(R.value,n=>(v(),r(C,{cols:`12`,sm:`6`,lg:`3`,key:n.label},{default:d(()=>[e(c,{elevation:`0`,lines:`two`,rounded:`lg`,border:``},{default:d(()=>[e(h,null,{append:d(()=>[e(u,{color:n.color,size:`30`},{default:d(()=>[o(m(n.icon),1)]),_:2},1032,[`color`])]),default:d(()=>[e(t,{class:`text-body-small`},{default:d(()=>[o(m(n.label),1)]),_:2},1024),e(t,null,{default:d(()=>[o(m(n.value),1)]),_:2},1024)]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1}),e(i,{elevation:`0`,border:``,rounded:`lg`},{default:d(()=>[e(a,{class:`d-flex align-center`},{default:d(()=>[f[5]||=o(` 告警历史 `,-1),e(T),e(E,{modelValue:I.value,"onUpdate:modelValue":[f[0]||=e=>I.value=e,f[1]||=e=>{F.value=1,U()}],items:z,"item-title":`label`,"item-value":`value`,label:`类型`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},clearable:``},null,8,[`modelValue`]),e(E,{modelValue:L.value,"onUpdate:modelValue":[f[2]||=e=>L.value=e,f[3]||=e=>{F.value=1,U()}],items:B,"item-title":`label`,"item-value":`value`,label:`状态`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},class:`ml-3`,clearable:``},null,8,[`modelValue`])]),_:1}),e(D,{items:N.value,headers:V,"items-length":P.value,loading:M.value,page:F.value,"items-per-page":20,hover:``,density:`comfortable`,"onUpdate:page":f[4]||=e=>{F.value=e,U()}},{"item.alert_type":d(({item:t})=>[e(x,{color:W(t.alert_type),size:`small`,variant:`tonal`,label:``},{prepend:d(()=>[e(u,{size:`12`},{default:d(()=>[o(m(G(t.alert_type)),1)]),_:2},1024)]),default:d(()=>[o(` `+m(t.alert_type),1)]),_:2},1032,[`color`])]),"item.server_name":d(({item:e})=>[g(`span`,O,m(e.server_name),1)]),"item.value":d(({item:e})=>[g(`span`,k,m(e.value),1)]),"item.is_recovery":d(({item:e})=>[e.is_recovery?(v(),r(x,{key:0,color:`success`,size:`small`,variant:`tonal`,label:``},{default:d(()=>[...f[6]||=[o(`已恢复`,-1)]]),_:1})):(v(),r(x,{key:1,color:`error`,size:`small`,variant:`tonal`,label:``},{default:d(()=>[...f[7]||=[o(`告警中`,-1)]]),_:1}))]),"item.created_at":d(({item:e})=>[g(`span`,A,m(e.created_at),1)]),"no-data":d(()=>[...f[8]||=[g(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`items-length`,`loading`,`page`])]),_:1})]),_:1}))}});export{j as default};
-1
View File
@@ -1 +0,0 @@
import{$n as e,A as t,Ar as n,Br as r,Cr as i,F as a,N as o,Rt as s,X as c,Xn as l,_r as u,ar as d,er as f,hr as p,ir as m,k as h,nr as g,or as _,pr as v,w as y}from"./index-Da-oPxMv.js";import{o as b,t as x}from"./VContainer-Dp-8rduj.js";import{t as S}from"./VChip-uNN5BiCx.js";import{t as C}from"./VSelect-B1u55nVH.js";import{t as w}from"./VDataTableServer-BLAQDDBi.js";import{n as T,t as E}from"./VRow-Rr6vqjSj.js";import{t as D}from"./VSpacer-ZMhNXvUl.js";var O={class:`font-weight-medium`},k={class:`text-body-2`},A={class:`text-caption text-medium-emphasis`},j=_({__name:`AlertsPage`,setup(_){let j=b(),M=n(!1),N=n([]),P=n(0),F=n(1),I=n(null),L=n(null),R=n([{label:`今日告警`,value:`0`,color:`blue`,icon:`mdi-alert`},{label:`活跃告警`,value:`0`,color:`orange`,icon:`mdi-bell-ring`},{label:`已恢复`,value:`0`,color:`green`,icon:`mdi-check-circle`},{label:`Top 服务器`,value:``,color:`red`,icon:`mdi-server`}]),z=[{label:`CPU`,value:`cpu`},{label:`内存`,value:`memory`},{label:`磁盘`,value:`disk`},{label:`连接`,value:`connection`}],B=[{label:`活跃`,value:`active`},{label:`已恢复`,value:`recovered`}],V=[{title:`类型`,key:`alert_type`,width:100},{title:`服务器`,key:`server_name`},{title:`详情`,key:`value`},{title:`状态`,key:`is_recovery`,width:100},{title:`时间`,key:`created_at`,width:160}];async function H(){try{let e=await s.get(`/alert-history/stats`);R.value[0].value=String(e.today||0),R.value[1].value=String(e.active||0),R.value[2].value=String(e.recovered||0),R.value[3].value=e.top_server||``}catch{j(`加载统计失败`,`error`)}}async function U(){M.value=!0;try{let e=await s.get(`/alert-history/`,{page:F.value,per_page:20,type:I.value||void 0,status:L.value||void 0});N.value=e.items||[],P.value=e.total||0}catch{N.value=[]}finally{M.value=!1}}function W(e){return e===`cpu`||e===`CPU`?`error`:e===`memory`||e===`内存`?`warning`:e===`disk`||e===`磁盘`?`info`:`primary`}function G(e){return e===`cpu`||e===`CPU`?`mdi-chip`:e===`memory`||e===`内存`?`mdi-memory`:e===`disk`||e===`磁盘`?`mdi-harddisk`:`mdi-lan-disconnect`}return v(()=>{H(),U()}),(n,s)=>(p(),f(x,{fluid:``,class:`pa-6`},{default:i(()=>[d(E,{class:`mb-4`},{default:i(()=>[(p(!0),g(l,null,u(R.value,e=>(p(),f(T,{cols:`12`,sm:`6`,lg:`3`,key:e.label},{default:i(()=>[d(y,{elevation:`0`,lines:`two`,rounded:`lg`,border:``},{default:i(()=>[d(h,null,{append:i(()=>[d(c,{color:e.color,size:`30`},{default:i(()=>[m(r(e.icon),1)]),_:2},1032,[`color`])]),default:i(()=>[d(t,{class:`text-body-small`},{default:i(()=>[m(r(e.label),1)]),_:2},1024),d(t,null,{default:i(()=>[m(r(e.value),1)]),_:2},1024)]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1}),d(o,{elevation:`0`,border:``,rounded:`lg`},{default:i(()=>[d(a,{class:`d-flex align-center`},{default:i(()=>[s[5]||=m(` 告警历史 `,-1),d(D),d(C,{modelValue:I.value,"onUpdate:modelValue":[s[0]||=e=>I.value=e,s[1]||=e=>{F.value=1,U()}],items:z,"item-title":`label`,"item-value":`value`,label:`类型`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},clearable:``},null,8,[`modelValue`]),d(C,{modelValue:L.value,"onUpdate:modelValue":[s[2]||=e=>L.value=e,s[3]||=e=>{F.value=1,U()}],items:B,"item-title":`label`,"item-value":`value`,label:`状态`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},class:`ml-3`,clearable:``},null,8,[`modelValue`])]),_:1}),d(w,{items:N.value,headers:V,"items-length":P.value,loading:M.value,page:F.value,"items-per-page":20,hover:``,density:`comfortable`,"onUpdate:page":s[4]||=e=>{F.value=e,U()}},{"item.alert_type":i(({item:e})=>[d(S,{color:W(e.alert_type),size:`small`,variant:`tonal`,label:``},{prepend:i(()=>[d(c,{size:`12`},{default:i(()=>[m(r(G(e.alert_type)),1)]),_:2},1024)]),default:i(()=>[m(` `+r(e.alert_type),1)]),_:2},1032,[`color`])]),"item.server_name":i(({item:t})=>[e(`span`,O,r(t.server_name),1)]),"item.value":i(({item:t})=>[e(`span`,k,r(t.value),1)]),"item.is_recovery":i(({item:e})=>[e.is_recovery?(p(),f(S,{key:0,color:`success`,size:`small`,variant:`tonal`,label:``},{default:i(()=>[...s[6]||=[m(`已恢复`,-1)]]),_:1})):(p(),f(S,{key:1,color:`error`,size:`small`,variant:`tonal`,label:``},{default:i(()=>[...s[7]||=[m(`告警中`,-1)]]),_:1}))]),"item.created_at":i(({item:t})=>[e(`span`,A,r(t.created_at),1)]),"no-data":i(()=>[...s[8]||=[e(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`items-length`,`loading`,`page`])]),_:1})]),_:1}))}});export{j as default};
+1
View File
@@ -0,0 +1 @@
import{n as e,t}from"./useSnackbar-DaoOJlMz.js";import{n,t as r}from"./VRow-Wg9QrEcV.js";import{Ht as i,Ir as a,Pr as o,Sr as s,Ur as c,X as l,Xr as u,Xt as d,_ as f,br as p,dr as m,gr as h,h as g,hr as _,l as v,m as y,ni as b,oi as x,vr as S,xr as C,y as w}from"./index-DZEQidkT.js";import{t as T}from"./usePageActivateRefresh-BmoMLdUi.js";import{n as E}from"./dataTable-esDY4cOk.js";import{r as D,t as O}from"./VCard-DiTj681b.js";import{t as k}from"./VSelect-CzNpj4qJ.js";import{t as A}from"./VDataTableServer-C1V2iC3x.js";var j={class:`font-weight-medium`},M={class:`text-body-2`},N={class:`text-caption text-medium-emphasis`},P=s({name:`AlertsPage`,__name:`AlertsPage`,setup(s){let P=t(),F=u(!1),I=u([]),L=u(0),R=u(1),z=u(null),B=u(null),V=u([{label:`今日告警`,value:`0`,color:`blue`,icon:`mdi-alert`},{label:`活跃告警`,value:`0`,color:`orange`,icon:`mdi-bell-ring`},{label:`已恢复`,value:`0`,color:`green`,icon:`mdi-check-circle`},{label:`Top 服务器`,value:``,color:`red`,icon:`mdi-server`}]),H=[{label:`CPU`,value:`cpu`},{label:`内存`,value:`memory`},{label:`磁盘`,value:`disk`},{label:`连接`,value:`connection`}],U=[{label:`活跃`,value:`active`},{label:`已恢复`,value:`recovered`}],W=E([{title:`类型`,key:`alert_type`,width:100},{title:`服务器`,key:`server_name`},{title:`详情`,key:`value`},{title:`状态`,key:`is_recovery`,width:100},{title:`时间`,key:`created_at`,width:160}]);async function G(){try{let e=await d.get(`/alert-history/stats`);V.value[0].value=String(e.today||0),V.value[1].value=String(e.active||0),V.value[2].value=String(e.recovered||0),V.value[3].value=e.top_server||``}catch{P(`加载统计失败`,`error`)}}async function K(e=!1){e||(F.value=!0);try{let e=await d.get(`/alert-history/`,{page:R.value,per_page:20,type:z.value||void 0,status:B.value||void 0});I.value=e.items||[],L.value=e.total||0}catch{I.value=[]}finally{e||(F.value=!1)}}function q(e){return e===`cpu`||e===`CPU`?`error`:e===`memory`||e===`内存`?`warning`:e===`disk`||e===`磁盘`?`info`:`primary`}function J(e){return e===`cpu`||e===`CPU`?`mdi-chip`:e===`memory`||e===`内存`?`mdi-memory`:e===`disk`||e===`磁盘`?`mdi-harddisk`:`mdi-lan-disconnect`}async function Y(e=!1){await Promise.all([G(),K(e)])}return T(e=>Y(e)),(t,s)=>(o(),h(e,{fluid:``,class:`pa-6`},{default:c(()=>[C(r,{class:`mb-4`},{default:c(()=>[(o(!0),S(m,null,a(V.value,e=>(o(),h(n,{cols:`12`,sm:`6`,lg:`3`,key:e.label},{default:c(()=>[C(v,{elevation:`0`,lines:`two`,rounded:`lg`,border:``},{default:c(()=>[C(y,null,{append:c(()=>[C(l,{color:e.color,size:`30`},{default:c(()=>[p(x(e.icon),1)]),_:2},1032,[`color`])]),default:c(()=>[C(g,{class:`text-body-small`},{default:c(()=>[p(x(e.label),1)]),_:2},1024),C(g,null,{default:c(()=>[p(x(e.value),1)]),_:2},1024)]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1}),C(O,{elevation:`0`,border:``,rounded:`lg`},{default:c(()=>[C(D,{class:`d-flex align-center`},{default:c(()=>[s[5]||=p(` 告警历史 `,-1),C(f),C(k,{modelValue:z.value,"onUpdate:modelValue":[s[0]||=e=>z.value=e,s[1]||=e=>{R.value=1,K()}],items:H,"item-title":`label`,"item-value":`value`,label:`类型`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},clearable:``},null,8,[`modelValue`]),C(k,{modelValue:B.value,"onUpdate:modelValue":[s[2]||=e=>B.value=e,s[3]||=e=>{R.value=1,K()}],items:U,"item-title":`label`,"item-value":`value`,label:`状态`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},class:`ml-3`,clearable:``},null,8,[`modelValue`])]),_:1}),C(A,{items:I.value,headers:b(W),"items-length":L.value,loading:F.value,page:R.value,"items-per-page":20,hover:``,density:`comfortable`,"onUpdate:page":s[4]||=e=>{R.value=e,K()}},{"item.alert_type":c(({item:e})=>[C(w,{color:q(e.alert_type),size:`small`,variant:`tonal`,label:``},{prepend:c(()=>[C(l,{size:`12`},{default:c(()=>[p(x(J(e.alert_type)),1)]),_:2},1024)]),default:c(()=>[p(` `+x(e.alert_type),1)]),_:2},1032,[`color`])]),"item.server_name":c(({item:e})=>[_(`span`,j,x(e.server_name),1)]),"item.value":c(({item:e})=>[_(`span`,M,x(e.value),1)]),"item.is_recovery":c(({item:e})=>[e.is_recovery?(o(),h(w,{key:0,color:`success`,size:`small`,variant:`tonal`,label:``},{default:c(()=>[...s[6]||=[p(`已恢复`,-1)]]),_:1})):(o(),h(w,{key:1,color:`error`,size:`small`,variant:`tonal`,label:``},{default:c(()=>[...s[7]||=[p(`告警中`,-1)]]),_:1}))]),"item.created_at":c(({item:e})=>[_(`span`,N,x(b(i)(e.created_at)),1)]),"no-data":c(()=>[...s[8]||=[_(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`headers`,`items-length`,`loading`,`page`])]),_:1})]),_:1}))}});export{P as default};
-1
View File
@@ -1 +0,0 @@
import{$n as e,A as t,Gn as n,Jn as r,M as i,P as a,Qn as o,Rt as s,T as c,Xn as l,Y as u,_r as d,cr as f,fr as p,jr as m,k as h,qn as g,tr as _,ur as v,xr as y}from"./index-BG5Fhi95.js";import{t as b}from"./useSnackbar-D7YOfKok.js";import{t as x}from"./VChip-zGyvDZnY.js";import{a as S}from"./VSelectionControl-Fx7Qu95Z.js";import{n as C,t as w}from"./VRow-BG4-qCOJ.js";import{t as T}from"./VSpacer-C9vyEkZS.js";import{t as E}from"./VSelect-DMWs0n08.js";import{t as D}from"./VDataTableServer-CWBoUoTe.js";var O={class:`font-weight-medium`},k={class:`text-body-2`},A={class:`text-caption text-medium-emphasis`},j=_({__name:`AlertsPage`,setup(_){let j=b(),M=y(!1),N=y([]),P=y(0),F=y(1),I=y(null),L=y(null),R=y([{label:`今日告警`,value:`0`,color:`blue`,icon:`mdi-alert`},{label:`活跃告警`,value:`0`,color:`orange`,icon:`mdi-bell-ring`},{label:`已恢复`,value:`0`,color:`green`,icon:`mdi-check-circle`},{label:`Top 服务器`,value:``,color:`red`,icon:`mdi-server`}]),z=[{label:`CPU`,value:`cpu`},{label:`内存`,value:`memory`},{label:`磁盘`,value:`disk`},{label:`连接`,value:`connection`}],B=[{label:`活跃`,value:`active`},{label:`已恢复`,value:`recovered`}],V=[{title:`类型`,key:`alert_type`,width:100},{title:`服务器`,key:`server_name`},{title:`详情`,key:`value`},{title:`状态`,key:`is_recovery`,width:100},{title:`时间`,key:`created_at`,width:160}];async function H(){try{let e=await s.get(`/alert-history/stats`);R.value[0].value=String(e.today||0),R.value[1].value=String(e.active||0),R.value[2].value=String(e.recovered||0),R.value[3].value=e.top_server||``}catch{j(`加载统计失败`,`error`)}}async function U(){M.value=!0;try{let e=await s.get(`/alert-history/`,{page:F.value,per_page:20,type:I.value||void 0,status:L.value||void 0});N.value=e.items||[],P.value=e.total||0}catch{N.value=[]}finally{M.value=!1}}function W(e){return e===`cpu`||e===`CPU`?`error`:e===`memory`||e===`内存`?`warning`:e===`disk`||e===`磁盘`?`info`:`primary`}function G(e){return e===`cpu`||e===`CPU`?`mdi-chip`:e===`memory`||e===`内存`?`mdi-memory`:e===`disk`||e===`磁盘`?`mdi-harddisk`:`mdi-lan-disconnect`}return f(()=>{H(),U()}),(s,f)=>(v(),r(S,{fluid:``,class:`pa-6`},{default:d(()=>[e(w,{class:`mb-4`},{default:d(()=>[(v(!0),l(n,null,p(R.value,n=>(v(),r(C,{cols:`12`,sm:`6`,lg:`3`,key:n.label},{default:d(()=>[e(c,{elevation:`0`,lines:`two`,rounded:`lg`,border:``},{default:d(()=>[e(h,null,{append:d(()=>[e(u,{color:n.color,size:`30`},{default:d(()=>[o(m(n.icon),1)]),_:2},1032,[`color`])]),default:d(()=>[e(t,{class:`text-body-small`},{default:d(()=>[o(m(n.label),1)]),_:2},1024),e(t,null,{default:d(()=>[o(m(n.value),1)]),_:2},1024)]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1}),e(i,{elevation:`0`,border:``,rounded:`lg`},{default:d(()=>[e(a,{class:`d-flex align-center`},{default:d(()=>[f[5]||=o(` 告警历史 `,-1),e(T),e(E,{modelValue:I.value,"onUpdate:modelValue":[f[0]||=e=>I.value=e,f[1]||=e=>{F.value=1,U()}],items:z,"item-title":`label`,"item-value":`value`,label:`类型`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},clearable:``},null,8,[`modelValue`]),e(E,{modelValue:L.value,"onUpdate:modelValue":[f[2]||=e=>L.value=e,f[3]||=e=>{F.value=1,U()}],items:B,"item-title":`label`,"item-value":`value`,label:`状态`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},class:`ml-3`,clearable:``},null,8,[`modelValue`])]),_:1}),e(D,{items:N.value,headers:V,"items-length":P.value,loading:M.value,page:F.value,"items-per-page":20,hover:``,density:`comfortable`,"onUpdate:page":f[4]||=e=>{F.value=e,U()}},{"item.alert_type":d(({item:t})=>[e(x,{color:W(t.alert_type),size:`small`,variant:`tonal`,label:``},{prepend:d(()=>[e(u,{size:`12`},{default:d(()=>[o(m(G(t.alert_type)),1)]),_:2},1024)]),default:d(()=>[o(` `+m(t.alert_type),1)]),_:2},1032,[`color`])]),"item.server_name":d(({item:e})=>[g(`span`,O,m(e.server_name),1)]),"item.value":d(({item:e})=>[g(`span`,k,m(e.value),1)]),"item.is_recovery":d(({item:e})=>[e.is_recovery?(v(),r(x,{key:0,color:`success`,size:`small`,variant:`tonal`,label:``},{default:d(()=>[...f[6]||=[o(`已恢复`,-1)]]),_:1})):(v(),r(x,{key:1,color:`error`,size:`small`,variant:`tonal`,label:``},{default:d(()=>[...f[7]||=[o(`告警中`,-1)]]),_:1}))]),"item.created_at":d(({item:e})=>[g(`span`,A,m(e.created_at),1)]),"no-data":d(()=>[...f[8]||=[g(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`items-length`,`loading`,`page`])]),_:1})]),_:1}))}});export{j as default};
-1
View File
@@ -1 +0,0 @@
import{$n as e,A as t,Ar as n,Gn as r,Jn as i,M as a,P as o,Qn as s,Rt as c,T as l,Xn as u,Y as d,br as f,cr as p,fr as m,gr as h,k as g,qn as _,tr as v,ur as y}from"./index-DGeqMJUY.js";import{t as b}from"./useSnackbar-D7YOfKok.js";import{t as x}from"./VChip-C4fNZBKq.js";import{a as S}from"./VSelectionControl-LeS8GYyn.js";import{n as C,t as w}from"./VRow-Bzc0SVl1.js";import{t as T}from"./VSpacer-mvmej6zi.js";import{t as E}from"./VSelect-BGIkwwyf.js";import{t as D}from"./VDataTableServer-BAJXfskE.js";var O={class:`font-weight-medium`},k={class:`text-body-2`},A={class:`text-caption text-medium-emphasis`},j=v({__name:`AlertsPage`,setup(v){let j=b(),M=f(!1),N=f([]),P=f(0),F=f(1),I=f(null),L=f(null),R=f([{label:`今日告警`,value:`0`,color:`blue`,icon:`mdi-alert`},{label:`活跃告警`,value:`0`,color:`orange`,icon:`mdi-bell-ring`},{label:`已恢复`,value:`0`,color:`green`,icon:`mdi-check-circle`},{label:`Top 服务器`,value:``,color:`red`,icon:`mdi-server`}]),z=[{label:`CPU`,value:`cpu`},{label:`内存`,value:`memory`},{label:`磁盘`,value:`disk`},{label:`连接`,value:`connection`}],B=[{label:`活跃`,value:`active`},{label:`已恢复`,value:`recovered`}],V=[{title:`类型`,key:`alert_type`,width:100},{title:`服务器`,key:`server_name`},{title:`详情`,key:`value`},{title:`状态`,key:`is_recovery`,width:100},{title:`时间`,key:`created_at`,width:160}];async function H(){try{let e=await c.get(`/alert-history/stats`);R.value[0].value=String(e.today||0),R.value[1].value=String(e.active||0),R.value[2].value=String(e.recovered||0),R.value[3].value=e.top_server||``}catch{j(`加载统计失败`,`error`)}}async function U(){M.value=!0;try{let e=await c.get(`/alert-history/`,{page:F.value,per_page:20,type:I.value||void 0,status:L.value||void 0});N.value=e.items||[],P.value=e.total||0}catch{N.value=[]}finally{M.value=!1}}function W(e){return e===`cpu`||e===`CPU`?`error`:e===`memory`||e===`内存`?`warning`:e===`disk`||e===`磁盘`?`info`:`primary`}function G(e){return e===`cpu`||e===`CPU`?`mdi-chip`:e===`memory`||e===`内存`?`mdi-memory`:e===`disk`||e===`磁盘`?`mdi-harddisk`:`mdi-lan-disconnect`}return p(()=>{H(),U()}),(c,f)=>(y(),i(S,{fluid:``,class:`pa-6`},{default:h(()=>[e(w,{class:`mb-4`},{default:h(()=>[(y(!0),u(r,null,m(R.value,r=>(y(),i(C,{cols:`12`,sm:`6`,lg:`3`,key:r.label},{default:h(()=>[e(l,{elevation:`0`,lines:`two`,rounded:`lg`,border:``},{default:h(()=>[e(g,null,{append:h(()=>[e(d,{color:r.color,size:`30`},{default:h(()=>[s(n(r.icon),1)]),_:2},1032,[`color`])]),default:h(()=>[e(t,{class:`text-body-small`},{default:h(()=>[s(n(r.label),1)]),_:2},1024),e(t,null,{default:h(()=>[s(n(r.value),1)]),_:2},1024)]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1}),e(a,{elevation:`0`,border:``,rounded:`lg`},{default:h(()=>[e(o,{class:`d-flex align-center`},{default:h(()=>[f[5]||=s(` 告警历史 `,-1),e(T),e(E,{modelValue:I.value,"onUpdate:modelValue":[f[0]||=e=>I.value=e,f[1]||=e=>{F.value=1,U()}],items:z,"item-title":`label`,"item-value":`value`,label:`类型`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},clearable:``},null,8,[`modelValue`]),e(E,{modelValue:L.value,"onUpdate:modelValue":[f[2]||=e=>L.value=e,f[3]||=e=>{F.value=1,U()}],items:B,"item-title":`label`,"item-value":`value`,label:`状态`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},class:`ml-3`,clearable:``},null,8,[`modelValue`])]),_:1}),e(D,{items:N.value,headers:V,"items-length":P.value,loading:M.value,page:F.value,"items-per-page":20,hover:``,density:`comfortable`,"onUpdate:page":f[4]||=e=>{F.value=e,U()}},{"item.alert_type":h(({item:t})=>[e(x,{color:W(t.alert_type),size:`small`,variant:`tonal`,label:``},{prepend:h(()=>[e(d,{size:`12`},{default:h(()=>[s(n(G(t.alert_type)),1)]),_:2},1024)]),default:h(()=>[s(` `+n(t.alert_type),1)]),_:2},1032,[`color`])]),"item.server_name":h(({item:e})=>[_(`span`,O,n(e.server_name),1)]),"item.value":h(({item:e})=>[_(`span`,k,n(e.value),1)]),"item.is_recovery":h(({item:e})=>[e.is_recovery?(y(),i(x,{key:0,color:`success`,size:`small`,variant:`tonal`,label:``},{default:h(()=>[...f[6]||=[s(`已恢复`,-1)]]),_:1})):(y(),i(x,{key:1,color:`error`,size:`small`,variant:`tonal`,label:``},{default:h(()=>[...f[7]||=[s(`告警中`,-1)]]),_:1}))]),"item.created_at":h(({item:e})=>[_(`span`,A,n(e.created_at),1)]),"no-data":h(()=>[...f[8]||=[_(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`items-length`,`loading`,`page`])]),_:1})]),_:1}))}});export{j as default};
-1
View File
@@ -1 +0,0 @@
import{$n as e,A as t,F as n,N as r,Rt as i,Vr as a,X as o,Xn as s,ar as c,er as l,gr as u,ir as d,jr as f,k as p,mr as m,nr as h,sr as g,vr as _,w as v,wr as y}from"./index-BMSEPayF.js";import{a as b}from"./VSelectionControl-CfnrPvec.js";import{n as x,t as S}from"./VRow-DyIp177c.js";import{t as C}from"./VSpacer-BEk4vEwa.js";import{t as w}from"./useSnackbar-D7YOfKok.js";import{t as T}from"./VChip-CtHbCV1q.js";import{t as E}from"./VSelect-7H9FhER6.js";import{t as D}from"./VDataTableServer-sv3SPEQw.js";var O={class:`font-weight-medium`},k={class:`text-body-2`},A={class:`text-caption text-medium-emphasis`},j=g({__name:`AlertsPage`,setup(g){let j=w(),M=f(!1),N=f([]),P=f(0),F=f(1),I=f(null),L=f(null),R=f([{label:`今日告警`,value:`0`,color:`blue`,icon:`mdi-alert`},{label:`活跃告警`,value:`0`,color:`orange`,icon:`mdi-bell-ring`},{label:`已恢复`,value:`0`,color:`green`,icon:`mdi-check-circle`},{label:`Top 服务器`,value:``,color:`red`,icon:`mdi-server`}]),z=[{label:`CPU`,value:`cpu`},{label:`内存`,value:`memory`},{label:`磁盘`,value:`disk`},{label:`连接`,value:`connection`}],B=[{label:`活跃`,value:`active`},{label:`已恢复`,value:`recovered`}],V=[{title:`类型`,key:`alert_type`,width:100},{title:`服务器`,key:`server_name`},{title:`详情`,key:`value`},{title:`状态`,key:`is_recovery`,width:100},{title:`时间`,key:`created_at`,width:160}];async function H(){try{let e=await i.get(`/alert-history/stats`);R.value[0].value=String(e.today||0),R.value[1].value=String(e.active||0),R.value[2].value=String(e.recovered||0),R.value[3].value=e.top_server||``}catch{j(`加载统计失败`,`error`)}}async function U(){M.value=!0;try{let e=await i.get(`/alert-history/`,{page:F.value,per_page:20,type:I.value||void 0,status:L.value||void 0});N.value=e.items||[],P.value=e.total||0}catch{N.value=[]}finally{M.value=!1}}function W(e){return e===`cpu`||e===`CPU`?`error`:e===`memory`||e===`内存`?`warning`:e===`disk`||e===`磁盘`?`info`:`primary`}function G(e){return e===`cpu`||e===`CPU`?`mdi-chip`:e===`memory`||e===`内存`?`mdi-memory`:e===`disk`||e===`磁盘`?`mdi-harddisk`:`mdi-lan-disconnect`}return m(()=>{H(),U()}),(i,f)=>(u(),l(b,{fluid:``,class:`pa-6`},{default:y(()=>[c(S,{class:`mb-4`},{default:y(()=>[(u(!0),h(s,null,_(R.value,e=>(u(),l(x,{cols:`12`,sm:`6`,lg:`3`,key:e.label},{default:y(()=>[c(v,{elevation:`0`,lines:`two`,rounded:`lg`,border:``},{default:y(()=>[c(p,null,{append:y(()=>[c(o,{color:e.color,size:`30`},{default:y(()=>[d(a(e.icon),1)]),_:2},1032,[`color`])]),default:y(()=>[c(t,{class:`text-body-small`},{default:y(()=>[d(a(e.label),1)]),_:2},1024),c(t,null,{default:y(()=>[d(a(e.value),1)]),_:2},1024)]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1}),c(r,{elevation:`0`,border:``,rounded:`lg`},{default:y(()=>[c(n,{class:`d-flex align-center`},{default:y(()=>[f[5]||=d(` 告警历史 `,-1),c(C),c(E,{modelValue:I.value,"onUpdate:modelValue":[f[0]||=e=>I.value=e,f[1]||=e=>{F.value=1,U()}],items:z,"item-title":`label`,"item-value":`value`,label:`类型`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},clearable:``},null,8,[`modelValue`]),c(E,{modelValue:L.value,"onUpdate:modelValue":[f[2]||=e=>L.value=e,f[3]||=e=>{F.value=1,U()}],items:B,"item-title":`label`,"item-value":`value`,label:`状态`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},class:`ml-3`,clearable:``},null,8,[`modelValue`])]),_:1}),c(D,{items:N.value,headers:V,"items-length":P.value,loading:M.value,page:F.value,"items-per-page":20,hover:``,density:`comfortable`,"onUpdate:page":f[4]||=e=>{F.value=e,U()}},{"item.alert_type":y(({item:e})=>[c(T,{color:W(e.alert_type),size:`small`,variant:`tonal`,label:``},{prepend:y(()=>[c(o,{size:`12`},{default:y(()=>[d(a(G(e.alert_type)),1)]),_:2},1024)]),default:y(()=>[d(` `+a(e.alert_type),1)]),_:2},1032,[`color`])]),"item.server_name":y(({item:t})=>[e(`span`,O,a(t.server_name),1)]),"item.value":y(({item:t})=>[e(`span`,k,a(t.value),1)]),"item.is_recovery":y(({item:e})=>[e.is_recovery?(u(),l(T,{key:0,color:`success`,size:`small`,variant:`tonal`,label:``},{default:y(()=>[...f[6]||=[d(`已恢复`,-1)]]),_:1})):(u(),l(T,{key:1,color:`error`,size:`small`,variant:`tonal`,label:``},{default:y(()=>[...f[7]||=[d(`告警中`,-1)]]),_:1}))]),"item.created_at":y(({item:t})=>[e(`span`,A,a(t.created_at),1)]),"no-data":y(()=>[...f[8]||=[e(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`items-length`,`loading`,`page`])]),_:1})]),_:1}))}});export{j as default};
-1
View File
@@ -1 +0,0 @@
import{A as e,F as t,N as n,Vr as r,Z as i,Zn as a,ar as o,er as s,gr as c,jr as l,k as u,mr as d,or as f,rr as p,sr as m,tr as h,vr as g,w as _,wr as v,zt as y}from"./index-AmyXDl4i.js";import{o as b,t as x}from"./VContainer-Dqw80qLq.js";import{t as S}from"./VChip-D3vZhJU3.js";import{t as C}from"./VSelect-DZ1vTOq0.js";import{t as w}from"./VDataTableServer-BYd6EHtC.js";import{n as T,t as E}from"./VRow-BOFF76XF.js";import{t as D}from"./VSpacer-e_QpSKFJ.js";var O={class:`font-weight-medium`},k={class:`text-body-2`},A={class:`text-caption text-medium-emphasis`},j=m({__name:`AlertsPage`,setup(m){let j=b(),M=l(!1),N=l([]),P=l(0),F=l(1),I=l(null),L=l(null),R=l([{label:`今日告警`,value:`0`,color:`blue`,icon:`mdi-alert`},{label:`活跃告警`,value:`0`,color:`orange`,icon:`mdi-bell-ring`},{label:`已恢复`,value:`0`,color:`green`,icon:`mdi-check-circle`},{label:`Top 服务器`,value:``,color:`red`,icon:`mdi-server`}]),z=[{label:`CPU`,value:`cpu`},{label:`内存`,value:`memory`},{label:`磁盘`,value:`disk`},{label:`连接`,value:`connection`}],B=[{label:`活跃`,value:`active`},{label:`已恢复`,value:`recovered`}],V=[{title:`类型`,key:`alert_type`,width:100},{title:`服务器`,key:`server_name`},{title:`详情`,key:`value`},{title:`状态`,key:`is_recovery`,width:100},{title:`时间`,key:`created_at`,width:160}];async function H(){try{let e=await y.get(`/alert-history/stats`);R.value[0].value=String(e.today||0),R.value[1].value=String(e.active||0),R.value[2].value=String(e.recovered||0),R.value[3].value=e.top_server||``}catch{j(`加载统计失败`,`error`)}}async function U(){M.value=!0;try{let e=await y.get(`/alert-history/`,{page:F.value,per_page:20,type:I.value||void 0,status:L.value||void 0});N.value=e.items||[],P.value=e.total||0}catch{N.value=[]}finally{M.value=!1}}function W(e){return e===`cpu`||e===`CPU`?`error`:e===`memory`||e===`内存`?`warning`:e===`disk`||e===`磁盘`?`info`:`primary`}function G(e){return e===`cpu`||e===`CPU`?`mdi-chip`:e===`memory`||e===`内存`?`mdi-memory`:e===`disk`||e===`磁盘`?`mdi-harddisk`:`mdi-lan-disconnect`}return d(()=>{H(),U()}),(l,d)=>(c(),h(x,{fluid:``,class:`pa-6`},{default:v(()=>[f(E,{class:`mb-4`},{default:v(()=>[(c(!0),p(a,null,g(R.value,t=>(c(),h(T,{cols:`12`,sm:`6`,lg:`3`,key:t.label},{default:v(()=>[f(_,{elevation:`0`,lines:`two`,rounded:`lg`,border:``},{default:v(()=>[f(u,null,{append:v(()=>[f(i,{color:t.color,size:`30`},{default:v(()=>[o(r(t.icon),1)]),_:2},1032,[`color`])]),default:v(()=>[f(e,{class:`text-body-small`},{default:v(()=>[o(r(t.label),1)]),_:2},1024),f(e,null,{default:v(()=>[o(r(t.value),1)]),_:2},1024)]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1}),f(n,{elevation:`0`,border:``,rounded:`lg`},{default:v(()=>[f(t,{class:`d-flex align-center`},{default:v(()=>[d[5]||=o(` 告警历史 `,-1),f(D),f(C,{modelValue:I.value,"onUpdate:modelValue":[d[0]||=e=>I.value=e,d[1]||=e=>{F.value=1,U()}],items:z,"item-title":`label`,"item-value":`value`,label:`类型`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},clearable:``},null,8,[`modelValue`]),f(C,{modelValue:L.value,"onUpdate:modelValue":[d[2]||=e=>L.value=e,d[3]||=e=>{F.value=1,U()}],items:B,"item-title":`label`,"item-value":`value`,label:`状态`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},class:`ml-3`,clearable:``},null,8,[`modelValue`])]),_:1}),f(w,{items:N.value,headers:V,"items-length":P.value,loading:M.value,page:F.value,"items-per-page":20,hover:``,density:`comfortable`,"onUpdate:page":d[4]||=e=>{F.value=e,U()}},{"item.alert_type":v(({item:e})=>[f(S,{color:W(e.alert_type),size:`small`,variant:`tonal`,label:``},{prepend:v(()=>[f(i,{size:`12`},{default:v(()=>[o(r(G(e.alert_type)),1)]),_:2},1024)]),default:v(()=>[o(` `+r(e.alert_type),1)]),_:2},1032,[`color`])]),"item.server_name":v(({item:e})=>[s(`span`,O,r(e.server_name),1)]),"item.value":v(({item:e})=>[s(`span`,k,r(e.value),1)]),"item.is_recovery":v(({item:e})=>[e.is_recovery?(c(),h(S,{key:0,color:`success`,size:`small`,variant:`tonal`,label:``},{default:v(()=>[...d[6]||=[o(`已恢复`,-1)]]),_:1})):(c(),h(S,{key:1,color:`error`,size:`small`,variant:`tonal`,label:``},{default:v(()=>[...d[7]||=[o(`告警中`,-1)]]),_:1}))]),"item.created_at":v(({item:e})=>[s(`span`,A,r(e.created_at),1)]),"no-data":v(()=>[...d[8]||=[s(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`items-length`,`loading`,`page`])]),_:1})]),_:1}))}});export{j as default};
-1
View File
@@ -1 +0,0 @@
import{$n as e,A as t,Ar as n,Br as r,Cr as i,F as a,N as o,Rt as s,X as c,Xn as l,_r as u,ar as d,er as f,hr as p,ir as m,k as h,nr as g,or as _,pr as v,w as y}from"./index-BH6wnuOf.js";import{t as b}from"./useSnackbar-D7YOfKok.js";import{t as x}from"./VChip-xxnQGsbF.js";import{t as S}from"./VSelect-CTCcPmMb.js";import{t as C}from"./VContainer-SxbigzU5.js";import{t as w}from"./VDataTableServer-Bkk6QoeQ.js";import{n as T,t as E}from"./VRow-DTpeXfrW.js";import{t as D}from"./VSpacer-Bqg4JLb5.js";var O={class:`font-weight-medium`},k={class:`text-body-2`},A={class:`text-caption text-medium-emphasis`},j=_({__name:`AlertsPage`,setup(_){let j=b(),M=n(!1),N=n([]),P=n(0),F=n(1),I=n(null),L=n(null),R=n([{label:`今日告警`,value:`0`,color:`blue`,icon:`mdi-alert`},{label:`活跃告警`,value:`0`,color:`orange`,icon:`mdi-bell-ring`},{label:`已恢复`,value:`0`,color:`green`,icon:`mdi-check-circle`},{label:`Top 服务器`,value:``,color:`red`,icon:`mdi-server`}]),z=[{label:`CPU`,value:`cpu`},{label:`内存`,value:`memory`},{label:`磁盘`,value:`disk`},{label:`连接`,value:`connection`}],B=[{label:`活跃`,value:`active`},{label:`已恢复`,value:`recovered`}],V=[{title:`类型`,key:`alert_type`,width:100},{title:`服务器`,key:`server_name`},{title:`详情`,key:`value`},{title:`状态`,key:`is_recovery`,width:100},{title:`时间`,key:`created_at`,width:160}];async function H(){try{let e=await s.get(`/alert-history/stats`);R.value[0].value=String(e.today||0),R.value[1].value=String(e.active||0),R.value[2].value=String(e.recovered||0),R.value[3].value=e.top_server||``}catch{j(`加载统计失败`,`error`)}}async function U(){M.value=!0;try{let e=await s.get(`/alert-history/`,{page:F.value,per_page:20,type:I.value||void 0,status:L.value||void 0});N.value=e.items||[],P.value=e.total||0}catch{N.value=[]}finally{M.value=!1}}function W(e){return e===`cpu`||e===`CPU`?`error`:e===`memory`||e===`内存`?`warning`:e===`disk`||e===`磁盘`?`info`:`primary`}function G(e){return e===`cpu`||e===`CPU`?`mdi-chip`:e===`memory`||e===`内存`?`mdi-memory`:e===`disk`||e===`磁盘`?`mdi-harddisk`:`mdi-lan-disconnect`}return v(()=>{H(),U()}),(n,s)=>(p(),f(C,{fluid:``,class:`pa-6`},{default:i(()=>[d(E,{class:`mb-4`},{default:i(()=>[(p(!0),g(l,null,u(R.value,e=>(p(),f(T,{cols:`12`,sm:`6`,lg:`3`,key:e.label},{default:i(()=>[d(y,{elevation:`0`,lines:`two`,rounded:`lg`,border:``},{default:i(()=>[d(h,null,{append:i(()=>[d(c,{color:e.color,size:`30`},{default:i(()=>[m(r(e.icon),1)]),_:2},1032,[`color`])]),default:i(()=>[d(t,{class:`text-body-small`},{default:i(()=>[m(r(e.label),1)]),_:2},1024),d(t,null,{default:i(()=>[m(r(e.value),1)]),_:2},1024)]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1}),d(o,{elevation:`0`,border:``,rounded:`lg`},{default:i(()=>[d(a,{class:`d-flex align-center`},{default:i(()=>[s[5]||=m(` 告警历史 `,-1),d(D),d(S,{modelValue:I.value,"onUpdate:modelValue":[s[0]||=e=>I.value=e,s[1]||=e=>{F.value=1,U()}],items:z,"item-title":`label`,"item-value":`value`,label:`类型`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},clearable:``},null,8,[`modelValue`]),d(S,{modelValue:L.value,"onUpdate:modelValue":[s[2]||=e=>L.value=e,s[3]||=e=>{F.value=1,U()}],items:B,"item-title":`label`,"item-value":`value`,label:`状态`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},class:`ml-3`,clearable:``},null,8,[`modelValue`])]),_:1}),d(w,{items:N.value,headers:V,"items-length":P.value,loading:M.value,page:F.value,"items-per-page":20,hover:``,density:`comfortable`,"onUpdate:page":s[4]||=e=>{F.value=e,U()}},{"item.alert_type":i(({item:e})=>[d(x,{color:W(e.alert_type),size:`small`,variant:`tonal`,label:``},{prepend:i(()=>[d(c,{size:`12`},{default:i(()=>[m(r(G(e.alert_type)),1)]),_:2},1024)]),default:i(()=>[m(` `+r(e.alert_type),1)]),_:2},1032,[`color`])]),"item.server_name":i(({item:t})=>[e(`span`,O,r(t.server_name),1)]),"item.value":i(({item:t})=>[e(`span`,k,r(t.value),1)]),"item.is_recovery":i(({item:e})=>[e.is_recovery?(p(),f(x,{key:0,color:`success`,size:`small`,variant:`tonal`,label:``},{default:i(()=>[...s[6]||=[m(`已恢复`,-1)]]),_:1})):(p(),f(x,{key:1,color:`error`,size:`small`,variant:`tonal`,label:``},{default:i(()=>[...s[7]||=[m(`告警中`,-1)]]),_:1}))]),"item.created_at":i(({item:t})=>[e(`span`,A,r(t.created_at),1)]),"no-data":i(()=>[...s[8]||=[e(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`items-length`,`loading`,`page`])]),_:1})]),_:1}))}});export{j as default};
-1
View File
@@ -1 +0,0 @@
import{A as e,F as t,N as n,Vr as r,Z as i,Zn as a,ar as o,er as s,gr as c,jr as l,k as u,mr as d,or as f,rr as p,sr as m,tr as h,vr as g,w as _,wr as v,zt as y}from"./index-CLVw8WaP.js";import{o as b,t as x}from"./VContainer-FlPueFWR.js";import{t as S}from"./VChip-CoJQpM5X.js";import{t as C}from"./VSelect-C9Xea3xT.js";import{t as w}from"./VDataTableServer-CihJhzNQ.js";import{n as T,t as E}from"./VRow-CGrZcJ6l.js";import{t as D}from"./VSpacer-BKvfPYW2.js";var O={class:`font-weight-medium`},k={class:`text-body-2`},A={class:`text-caption text-medium-emphasis`},j=m({__name:`AlertsPage`,setup(m){let j=b(),M=l(!1),N=l([]),P=l(0),F=l(1),I=l(null),L=l(null),R=l([{label:`今日告警`,value:`0`,color:`blue`,icon:`mdi-alert`},{label:`活跃告警`,value:`0`,color:`orange`,icon:`mdi-bell-ring`},{label:`已恢复`,value:`0`,color:`green`,icon:`mdi-check-circle`},{label:`Top 服务器`,value:``,color:`red`,icon:`mdi-server`}]),z=[{label:`CPU`,value:`cpu`},{label:`内存`,value:`memory`},{label:`磁盘`,value:`disk`},{label:`连接`,value:`connection`}],B=[{label:`活跃`,value:`active`},{label:`已恢复`,value:`recovered`}],V=[{title:`类型`,key:`alert_type`,width:100},{title:`服务器`,key:`server_name`},{title:`详情`,key:`value`},{title:`状态`,key:`is_recovery`,width:100},{title:`时间`,key:`created_at`,width:160}];async function H(){try{let e=await y.get(`/alert-history/stats`);R.value[0].value=String(e.today||0),R.value[1].value=String(e.active||0),R.value[2].value=String(e.recovered||0),R.value[3].value=e.top_server||``}catch{j(`加载统计失败`,`error`)}}async function U(){M.value=!0;try{let e=await y.get(`/alert-history/`,{page:F.value,per_page:20,type:I.value||void 0,status:L.value||void 0});N.value=e.items||[],P.value=e.total||0}catch{N.value=[]}finally{M.value=!1}}function W(e){return e===`cpu`||e===`CPU`?`error`:e===`memory`||e===`内存`?`warning`:e===`disk`||e===`磁盘`?`info`:`primary`}function G(e){return e===`cpu`||e===`CPU`?`mdi-chip`:e===`memory`||e===`内存`?`mdi-memory`:e===`disk`||e===`磁盘`?`mdi-harddisk`:`mdi-lan-disconnect`}return d(()=>{H(),U()}),(l,d)=>(c(),h(x,{fluid:``,class:`pa-6`},{default:v(()=>[f(E,{class:`mb-4`},{default:v(()=>[(c(!0),p(a,null,g(R.value,t=>(c(),h(T,{cols:`12`,sm:`6`,lg:`3`,key:t.label},{default:v(()=>[f(_,{elevation:`0`,lines:`two`,rounded:`lg`,border:``},{default:v(()=>[f(u,null,{append:v(()=>[f(i,{color:t.color,size:`30`},{default:v(()=>[o(r(t.icon),1)]),_:2},1032,[`color`])]),default:v(()=>[f(e,{class:`text-body-small`},{default:v(()=>[o(r(t.label),1)]),_:2},1024),f(e,null,{default:v(()=>[o(r(t.value),1)]),_:2},1024)]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1}),f(n,{elevation:`0`,border:``,rounded:`lg`},{default:v(()=>[f(t,{class:`d-flex align-center`},{default:v(()=>[d[5]||=o(` 告警历史 `,-1),f(D),f(C,{modelValue:I.value,"onUpdate:modelValue":[d[0]||=e=>I.value=e,d[1]||=e=>{F.value=1,U()}],items:z,"item-title":`label`,"item-value":`value`,label:`类型`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},clearable:``},null,8,[`modelValue`]),f(C,{modelValue:L.value,"onUpdate:modelValue":[d[2]||=e=>L.value=e,d[3]||=e=>{F.value=1,U()}],items:B,"item-title":`label`,"item-value":`value`,label:`状态`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},class:`ml-3`,clearable:``},null,8,[`modelValue`])]),_:1}),f(w,{items:N.value,headers:V,"items-length":P.value,loading:M.value,page:F.value,"items-per-page":20,hover:``,density:`comfortable`,"onUpdate:page":d[4]||=e=>{F.value=e,U()}},{"item.alert_type":v(({item:e})=>[f(S,{color:W(e.alert_type),size:`small`,variant:`tonal`,label:``},{prepend:v(()=>[f(i,{size:`12`},{default:v(()=>[o(r(G(e.alert_type)),1)]),_:2},1024)]),default:v(()=>[o(` `+r(e.alert_type),1)]),_:2},1032,[`color`])]),"item.server_name":v(({item:e})=>[s(`span`,O,r(e.server_name),1)]),"item.value":v(({item:e})=>[s(`span`,k,r(e.value),1)]),"item.is_recovery":v(({item:e})=>[e.is_recovery?(c(),h(S,{key:0,color:`success`,size:`small`,variant:`tonal`,label:``},{default:v(()=>[...d[6]||=[o(`已恢复`,-1)]]),_:1})):(c(),h(S,{key:1,color:`error`,size:`small`,variant:`tonal`,label:``},{default:v(()=>[...d[7]||=[o(`告警中`,-1)]]),_:1}))]),"item.created_at":v(({item:e})=>[s(`span`,A,r(e.created_at),1)]),"no-data":v(()=>[...d[8]||=[s(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`items-length`,`loading`,`page`])]),_:1})]),_:1}))}});export{j as default};
-1
View File
@@ -1 +0,0 @@
import{_ as e,t,v as n}from"./VSelect-DOfwsXpH.js";import{n as r,t as i}from"./VRow-CVgvqlxo.js";import{Jr as a,Jt as o,Mr as s,Pr as c,Vr as l,X as u,_ as d,br as f,ei as p,gr as m,h,ii as g,l as _,lr as v,m as y,mr as b,pr as x,vr as S,y as C,yr as w}from"./index-D4oqMAQw.js";import{t as T}from"./usePageActivateRefresh-BGKv6u4c.js";import{n as E}from"./dataTable-esDY4cOk.js";import{_ as D,y as O}from"./VTextField-jVgRRmSB.js";import{t as k}from"./VDataTableServer-aSsB_Gjg.js";var A={class:`font-weight-medium`},j={class:`text-body-2`},M={class:`text-caption text-medium-emphasis`},N=f({name:`AlertsPage`,__name:`AlertsPage`,setup(f){let N=e(),P=a(!1),F=a([]),I=a(0),L=a(1),R=a(null),z=a(null),B=a([{label:`今日告警`,value:`0`,color:`blue`,icon:`mdi-alert`},{label:`活跃告警`,value:`0`,color:`orange`,icon:`mdi-bell-ring`},{label:`已恢复`,value:`0`,color:`green`,icon:`mdi-check-circle`},{label:`Top 服务器`,value:``,color:`red`,icon:`mdi-server`}]),V=[{label:`CPU`,value:`cpu`},{label:`内存`,value:`memory`},{label:`磁盘`,value:`disk`},{label:`连接`,value:`connection`}],H=[{label:`活跃`,value:`active`},{label:`已恢复`,value:`recovered`}],U=E([{title:`类型`,key:`alert_type`,width:100},{title:`服务器`,key:`server_name`},{title:`详情`,key:`value`},{title:`状态`,key:`is_recovery`,width:100},{title:`时间`,key:`created_at`,width:160}]);async function W(){try{let e=await o.get(`/alert-history/stats`);B.value[0].value=String(e.today||0),B.value[1].value=String(e.active||0),B.value[2].value=String(e.recovered||0),B.value[3].value=e.top_server||``}catch{N(`加载统计失败`,`error`)}}async function G(e=!1){e||(P.value=!0);try{let e=await o.get(`/alert-history/`,{page:L.value,per_page:20,type:R.value||void 0,status:z.value||void 0});F.value=e.items||[],I.value=e.total||0}catch{F.value=[]}finally{e||(P.value=!1)}}function K(e){return e===`cpu`||e===`CPU`?`error`:e===`memory`||e===`内存`?`warning`:e===`disk`||e===`磁盘`?`info`:`primary`}function q(e){return e===`cpu`||e===`CPU`?`mdi-chip`:e===`memory`||e===`内存`?`mdi-memory`:e===`disk`||e===`磁盘`?`mdi-harddisk`:`mdi-lan-disconnect`}async function J(e=!1){await Promise.all([W(),G(e)])}return T(e=>J(e)),(e,a)=>(s(),b(n,{fluid:``,class:`pa-6`},{default:l(()=>[w(i,{class:`mb-4`},{default:l(()=>[(s(!0),m(v,null,c(B.value,e=>(s(),b(r,{cols:`12`,sm:`6`,lg:`3`,key:e.label},{default:l(()=>[w(_,{elevation:`0`,lines:`two`,rounded:`lg`,border:``},{default:l(()=>[w(y,null,{append:l(()=>[w(u,{color:e.color,size:`30`},{default:l(()=>[S(g(e.icon),1)]),_:2},1032,[`color`])]),default:l(()=>[w(h,{class:`text-body-small`},{default:l(()=>[S(g(e.label),1)]),_:2},1024),w(h,null,{default:l(()=>[S(g(e.value),1)]),_:2},1024)]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1}),w(D,{elevation:`0`,border:``,rounded:`lg`},{default:l(()=>[w(O,{class:`d-flex align-center`},{default:l(()=>[a[5]||=S(` 告警历史 `,-1),w(d),w(t,{modelValue:R.value,"onUpdate:modelValue":[a[0]||=e=>R.value=e,a[1]||=e=>{L.value=1,G()}],items:V,"item-title":`label`,"item-value":`value`,label:`类型`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},clearable:``},null,8,[`modelValue`]),w(t,{modelValue:z.value,"onUpdate:modelValue":[a[2]||=e=>z.value=e,a[3]||=e=>{L.value=1,G()}],items:H,"item-title":`label`,"item-value":`value`,label:`状态`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},class:`ml-3`,clearable:``},null,8,[`modelValue`])]),_:1}),w(k,{items:F.value,headers:p(U),"items-length":I.value,loading:P.value,page:L.value,"items-per-page":20,hover:``,density:`comfortable`,"onUpdate:page":a[4]||=e=>{L.value=e,G()}},{"item.alert_type":l(({item:e})=>[w(C,{color:K(e.alert_type),size:`small`,variant:`tonal`,label:``},{prepend:l(()=>[w(u,{size:`12`},{default:l(()=>[S(g(q(e.alert_type)),1)]),_:2},1024)]),default:l(()=>[S(` `+g(e.alert_type),1)]),_:2},1032,[`color`])]),"item.server_name":l(({item:e})=>[x(`span`,A,g(e.server_name),1)]),"item.value":l(({item:e})=>[x(`span`,j,g(e.value),1)]),"item.is_recovery":l(({item:e})=>[e.is_recovery?(s(),b(C,{key:0,color:`success`,size:`small`,variant:`tonal`,label:``},{default:l(()=>[...a[6]||=[S(`已恢复`,-1)]]),_:1})):(s(),b(C,{key:1,color:`error`,size:`small`,variant:`tonal`,label:``},{default:l(()=>[...a[7]||=[S(`告警中`,-1)]]),_:1}))]),"item.created_at":l(({item:e})=>[x(`span`,M,g(e.created_at),1)]),"no-data":l(()=>[...a[8]||=[x(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`headers`,`items-length`,`loading`,`page`])]),_:1})]),_:1}))}});export{N as default};
-1
View File
@@ -1 +0,0 @@
import{A as e,Er as t,F as n,N as r,Nr as i,Ur as a,Z as o,Zn as s,_r as c,ar as l,er as u,hr as d,k as f,or as p,rr as m,sr as h,tr as g,w as _,yr as v,zr as y,zt as b}from"./index-Hwt84p4T.js";import{o as x,t as S}from"./VContainer-DVdhFFDK.js";import{n as C}from"./dataTable-esDY4cOk.js";import{t as w}from"./VChip-BOlm6rKc.js";import{t as T}from"./VSelect-Ce9_iAcL.js";import{t as E}from"./VDataTableServer-Ch0R1ufY.js";import{n as D,t as O}from"./VRow-CW1Gj3Lm.js";import{t as k}from"./VSpacer-Dz9VwXyE.js";var A={class:`font-weight-medium`},j={class:`text-body-2`},M={class:`text-caption text-medium-emphasis`},N=h({__name:`AlertsPage`,setup(h){let N=x(),P=i(!1),F=i([]),I=i(0),L=i(1),R=i(null),z=i(null),B=i([{label:`今日告警`,value:`0`,color:`blue`,icon:`mdi-alert`},{label:`活跃告警`,value:`0`,color:`orange`,icon:`mdi-bell-ring`},{label:`已恢复`,value:`0`,color:`green`,icon:`mdi-check-circle`},{label:`Top 服务器`,value:``,color:`red`,icon:`mdi-server`}]),V=[{label:`CPU`,value:`cpu`},{label:`内存`,value:`memory`},{label:`磁盘`,value:`disk`},{label:`连接`,value:`connection`}],H=[{label:`活跃`,value:`active`},{label:`已恢复`,value:`recovered`}],U=C([{title:`类型`,key:`alert_type`,width:100},{title:`服务器`,key:`server_name`},{title:`详情`,key:`value`},{title:`状态`,key:`is_recovery`,width:100},{title:`时间`,key:`created_at`,width:160}]);async function W(){try{let e=await b.get(`/alert-history/stats`);B.value[0].value=String(e.today||0),B.value[1].value=String(e.active||0),B.value[2].value=String(e.recovered||0),B.value[3].value=e.top_server||``}catch{N(`加载统计失败`,`error`)}}async function G(){P.value=!0;try{let e=await b.get(`/alert-history/`,{page:L.value,per_page:20,type:R.value||void 0,status:z.value||void 0});F.value=e.items||[],I.value=e.total||0}catch{F.value=[]}finally{P.value=!1}}function K(e){return e===`cpu`||e===`CPU`?`error`:e===`memory`||e===`内存`?`warning`:e===`disk`||e===`磁盘`?`info`:`primary`}function q(e){return e===`cpu`||e===`CPU`?`mdi-chip`:e===`memory`||e===`内存`?`mdi-memory`:e===`disk`||e===`磁盘`?`mdi-harddisk`:`mdi-lan-disconnect`}return d(()=>{W(),G()}),(i,d)=>(c(),g(S,{fluid:``,class:`pa-6`},{default:t(()=>[p(O,{class:`mb-4`},{default:t(()=>[(c(!0),m(s,null,v(B.value,n=>(c(),g(D,{cols:`12`,sm:`6`,lg:`3`,key:n.label},{default:t(()=>[p(_,{elevation:`0`,lines:`two`,rounded:`lg`,border:``},{default:t(()=>[p(f,null,{append:t(()=>[p(o,{color:n.color,size:`30`},{default:t(()=>[l(a(n.icon),1)]),_:2},1032,[`color`])]),default:t(()=>[p(e,{class:`text-body-small`},{default:t(()=>[l(a(n.label),1)]),_:2},1024),p(e,null,{default:t(()=>[l(a(n.value),1)]),_:2},1024)]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1}),p(r,{elevation:`0`,border:``,rounded:`lg`},{default:t(()=>[p(n,{class:`d-flex align-center`},{default:t(()=>[d[5]||=l(` 告警历史 `,-1),p(k),p(T,{modelValue:R.value,"onUpdate:modelValue":[d[0]||=e=>R.value=e,d[1]||=e=>{L.value=1,G()}],items:V,"item-title":`label`,"item-value":`value`,label:`类型`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},clearable:``},null,8,[`modelValue`]),p(T,{modelValue:z.value,"onUpdate:modelValue":[d[2]||=e=>z.value=e,d[3]||=e=>{L.value=1,G()}],items:H,"item-title":`label`,"item-value":`value`,label:`状态`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},class:`ml-3`,clearable:``},null,8,[`modelValue`])]),_:1}),p(E,{items:F.value,headers:y(U),"items-length":I.value,loading:P.value,page:L.value,"items-per-page":20,hover:``,density:`comfortable`,"onUpdate:page":d[4]||=e=>{L.value=e,G()}},{"item.alert_type":t(({item:e})=>[p(w,{color:K(e.alert_type),size:`small`,variant:`tonal`,label:``},{prepend:t(()=>[p(o,{size:`12`},{default:t(()=>[l(a(q(e.alert_type)),1)]),_:2},1024)]),default:t(()=>[l(` `+a(e.alert_type),1)]),_:2},1032,[`color`])]),"item.server_name":t(({item:e})=>[u(`span`,A,a(e.server_name),1)]),"item.value":t(({item:e})=>[u(`span`,j,a(e.value),1)]),"item.is_recovery":t(({item:e})=>[e.is_recovery?(c(),g(w,{key:0,color:`success`,size:`small`,variant:`tonal`,label:``},{default:t(()=>[...d[6]||=[l(`已恢复`,-1)]]),_:1})):(c(),g(w,{key:1,color:`error`,size:`small`,variant:`tonal`,label:``},{default:t(()=>[...d[7]||=[l(`告警中`,-1)]]),_:1}))]),"item.created_at":t(({item:e})=>[u(`span`,M,a(e.created_at),1)]),"no-data":t(()=>[...d[8]||=[u(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`headers`,`items-length`,`loading`,`page`])]),_:1})]),_:1}))}});export{N as default};

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