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:
@@ -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
|
||||
|
||||
@@ -26,6 +26,7 @@ ENV/
|
||||
.megamemory/
|
||||
.playwright-mcp/
|
||||
frontend/test-results/
|
||||
frontend/e2e/.auth/
|
||||
tmp/
|
||||
|
||||
# IDE
|
||||
|
||||
@@ -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
@@ -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` | 页域 E2E(18) |
|
||||
| `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 3–6 | PASS(本审计前) |
|
||||
| MCP-INST-001 各 step UI | ⏭ 用户确认不纳入验收 |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] Wave 1–5 矩阵 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 1–5 实施:先补 `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 1–4 落地
|
||||
|
||||
## 摘要
|
||||
|
||||
按计划补齐 IT/CH/E2E 自动化:推送 accepted、链条 WS、12 条集成测试、5 条链条测试、14 条 Playwright 页面 spec;CI 分层跑 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 1–4,关闭 `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 1–5 + E2E 35/35)
|
||||
|
||||
**日期**: 2026-06-09
|
||||
|
||||
## 摘要
|
||||
|
||||
补齐 `test-case-matrix` 中 IT/CH/E2E 缺口;Playwright 全量 35/35 green;pytest 455 passed(含 slow)。
|
||||
|
||||
## 动机
|
||||
|
||||
- Wave 1–5 仅有 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 case,serial) |
|
||||
| 验收聚合 | `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 — 目录与标记(1–2 周)
|
||||
|
||||
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 拆分(2–3 周)
|
||||
|
||||
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 + DB,httpx 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 spec(L4)
|
||||
|
||||
### 安全与性能约束
|
||||
|
||||
- 测试账号:`NEXUS_TEST_ADMIN_*`,禁止写生产密钥入 git
|
||||
- E2E 默认 `127.0.0.1:8600`;生产 URL 仅只读冒烟且需用户批准
|
||||
- 链条测试子机:标记 `is_test=1` 或使用 ID 段约定(文档化)
|
||||
- 单 CI job pytest 目标 < 10min;chain/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 流
|
||||
- [ ] CI:PR 跑 L1+L2;main 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_* 后不再发 Telegram(WebSocket/日志仍记录)。
|
||||
2. **服务器行内详情** — 点击名称展开 `ServerInlineDetail`(expanded id 与 `item-value` 数字一致)。
|
||||
|
||||
## 浏览器终验(用户)
|
||||
|
||||
- [ ] 服务器页 → 点击名称 → 展开 CPU/内存/同步日志
|
||||
- [ ] 设置页 → 关闭「CPU 超阈值告警」→ 确认不再收到 Telegram(WebSocket 告警仍可有)
|
||||
@@ -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 1–5 测试矩阵对应的 Playwright 全量 35/35 green**,可与 `tests/integration` + `tests/chain`(27 passed)一并作为 L4 证据。
|
||||
@@ -0,0 +1,13 @@
|
||||
# 部署验证 — 推送后台化 + 422 中文化 + 隐藏 CSV(2026-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 轮询终态仍标待写。
|
||||
@@ -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. 写报告;若 PASS,7 日内补 `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;仅用于部署后探索与用例发现。
|
||||
@@ -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.mjs(mock 子机+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) | ~3(manual/安装) | ~21 |
|
||||
| **合计** | **~133** | **~36** | **~169** |
|
||||
|
||||
---
|
||||
|
||||
## PR 检查清单
|
||||
|
||||
- [ ] 改动域在表中至少更新 1 行状态
|
||||
- [ ] 新 API 必有 `IT-*`;新纯函数必有 `UT-*`
|
||||
- [ ] UI 行为变更必有 `E2E-*` 或 `MCP-*` 计划
|
||||
- [ ] 破坏性 E2E 仅对测试子机
|
||||
@@ -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'
|
||||
@@ -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()
|
||||
|
||||
@@ -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/
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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 })
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
@@ -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([])
|
||||
})
|
||||
@@ -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 })
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
@@ -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 })
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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 })
|
||||
})
|
||||
@@ -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 })
|
||||
})
|
||||
@@ -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",
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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",)),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# L2 集成测试
|
||||
|
||||
API + 内存 SQLite / mock Redis;复杂场景用 `api_client` + `test_admin` fixtures。
|
||||
|
||||
- 编号:`IT-<域>-<序号>`
|
||||
- 运行:`pytest tests/integration -m integration -q`
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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,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
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# L1 单元测试
|
||||
|
||||
新用例写在此目录;现有 `tests/test_*.py` 逐步归类迁入(不阻塞门控)。
|
||||
|
||||
- 编号:`UT-<域>-<序号>` → `docs/testing/test-case-matrix.md`
|
||||
- 运行:`pytest tests/unit -m unit -q`
|
||||
@@ -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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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};
|
||||
@@ -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 +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 +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 +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 +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 +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 +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 +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 +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
Reference in New Issue
Block a user