Spaces:
Running
Running
zRzRzRzRzRzRzR commited on
Commit ·
d22337b
1
Parent(s): 2eb87e3
feat: sync latest OpenVuln frontend
Browse files- .gitattributes +1 -0
- Dockerfile +1 -0
- package.json +3 -0
- pnpm-lock.yaml +38 -0
- public/hero-bg-poster.jpg +0 -0
- public/hero-bg.mp4 +3 -0
- public/logo.svg +4 -0
- src/components/AuthButton.tsx +13 -39
- src/components/Button.tsx +4 -4
- src/components/ConfirmDialog.tsx +1 -1
- src/components/HeroStats.tsx +0 -115
- src/components/Layout.tsx +17 -8
- src/components/NotificationBell.tsx +14 -30
- src/components/ProjectRow.tsx +9 -10
- src/components/RepoSubmitForm.tsx +18 -14
- src/components/ScanDurationNotice.tsx +1 -1
- src/components/SeverityBar.tsx +7 -7
- src/features/home/ProductHomePage.tsx +496 -331
- src/features/my/MyProjectsPage.tsx +1 -1
- src/features/project/OwnerFindings.tsx +2 -2
- src/features/project/ProjectPage.tsx +27 -27
- src/features/project/VersionBar.tsx +1 -1
- src/features/submit/SubmitPage.tsx +3 -3
- src/index.css +158 -52
- src/main.tsx +5 -0
- src/shared/api/client.ts +1 -1
- tailwind.config.js +33 -31
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
CHANGED
|
@@ -9,6 +9,7 @@ RUN pnpm install --frozen-lockfile
|
|
| 9 |
|
| 10 |
COPY index.html postcss.config.js tailwind.config.js tsconfig.json vite.config.ts ./
|
| 11 |
COPY src ./src
|
|
|
|
| 12 |
ARG VITE_API_BASE_URL=https://openvuln.vulnhunter.pro
|
| 13 |
ARG VITE_GITHUB_REPO_URL=https://github.com/Clouditera/OpenVuln
|
| 14 |
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL \
|
|
|
|
| 9 |
|
| 10 |
COPY index.html postcss.config.js tailwind.config.js tsconfig.json vite.config.ts ./
|
| 11 |
COPY src ./src
|
| 12 |
+
COPY public ./public
|
| 13 |
ARG VITE_API_BASE_URL=https://openvuln.vulnhunter.pro
|
| 14 |
ARG VITE_GITHUB_REPO_URL=https://github.com/Clouditera/OpenVuln
|
| 15 |
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL \
|
package.json
CHANGED
|
@@ -8,6 +8,9 @@
|
|
| 8 |
"build": "tsc -b && vite build"
|
| 9 |
},
|
| 10 |
"dependencies": {
|
|
|
|
|
|
|
|
|
|
| 11 |
"@tanstack/react-query": "^5.62.7",
|
| 12 |
"js-yaml": "^5.2.3",
|
| 13 |
"lucide-react": "^0.469.0",
|
|
|
|
| 8 |
"build": "tsc -b && vite build"
|
| 9 |
},
|
| 10 |
"dependencies": {
|
| 11 |
+
"@fontsource-variable/inter": "^5.3.0",
|
| 12 |
+
"@fontsource-variable/space-grotesk": "^5.3.0",
|
| 13 |
+
"@fontsource/ibm-plex-mono": "^5.3.0",
|
| 14 |
"@tanstack/react-query": "^5.62.7",
|
| 15 |
"js-yaml": "^5.2.3",
|
| 16 |
"lucide-react": "^0.469.0",
|
pnpm-lock.yaml
CHANGED
|
@@ -8,6 +8,15 @@ importers:
|
|
| 8 |
|
| 9 |
.:
|
| 10 |
dependencies:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
'@tanstack/react-query':
|
| 12 |
specifier: ^5.62.7
|
| 13 |
version: 5.101.4(react@18.3.1)
|
|
@@ -300,6 +309,15 @@ packages:
|
|
| 300 |
cpu: [x64]
|
| 301 |
os: [win32]
|
| 302 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
'@jridgewell/gen-mapping@0.3.13':
|
| 304 |
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
| 305 |
|
|
@@ -321,6 +339,7 @@ packages:
|
|
| 321 |
engines: {node: ^22.20 || ^24.12 || >=25}
|
| 322 |
cpu: [x64]
|
| 323 |
os: [linux]
|
|
|
|
| 324 |
|
| 325 |
'@nodelib/fs.scandir@2.1.5':
|
| 326 |
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
|
@@ -375,66 +394,79 @@ packages:
|
|
| 375 |
resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==}
|
| 376 |
cpu: [arm]
|
| 377 |
os: [linux]
|
|
|
|
| 378 |
|
| 379 |
'@rollup/rollup-linux-arm-musleabihf@4.62.4':
|
| 380 |
resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==}
|
| 381 |
cpu: [arm]
|
| 382 |
os: [linux]
|
|
|
|
| 383 |
|
| 384 |
'@rollup/rollup-linux-arm64-gnu@4.62.4':
|
| 385 |
resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==}
|
| 386 |
cpu: [arm64]
|
| 387 |
os: [linux]
|
|
|
|
| 388 |
|
| 389 |
'@rollup/rollup-linux-arm64-musl@4.62.4':
|
| 390 |
resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==}
|
| 391 |
cpu: [arm64]
|
| 392 |
os: [linux]
|
|
|
|
| 393 |
|
| 394 |
'@rollup/rollup-linux-loong64-gnu@4.62.4':
|
| 395 |
resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==}
|
| 396 |
cpu: [loong64]
|
| 397 |
os: [linux]
|
|
|
|
| 398 |
|
| 399 |
'@rollup/rollup-linux-loong64-musl@4.62.4':
|
| 400 |
resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==}
|
| 401 |
cpu: [loong64]
|
| 402 |
os: [linux]
|
|
|
|
| 403 |
|
| 404 |
'@rollup/rollup-linux-ppc64-gnu@4.62.4':
|
| 405 |
resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==}
|
| 406 |
cpu: [ppc64]
|
| 407 |
os: [linux]
|
|
|
|
| 408 |
|
| 409 |
'@rollup/rollup-linux-ppc64-musl@4.62.4':
|
| 410 |
resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==}
|
| 411 |
cpu: [ppc64]
|
| 412 |
os: [linux]
|
|
|
|
| 413 |
|
| 414 |
'@rollup/rollup-linux-riscv64-gnu@4.62.4':
|
| 415 |
resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==}
|
| 416 |
cpu: [riscv64]
|
| 417 |
os: [linux]
|
|
|
|
| 418 |
|
| 419 |
'@rollup/rollup-linux-riscv64-musl@4.62.4':
|
| 420 |
resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==}
|
| 421 |
cpu: [riscv64]
|
| 422 |
os: [linux]
|
|
|
|
| 423 |
|
| 424 |
'@rollup/rollup-linux-s390x-gnu@4.62.4':
|
| 425 |
resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==}
|
| 426 |
cpu: [s390x]
|
| 427 |
os: [linux]
|
|
|
|
| 428 |
|
| 429 |
'@rollup/rollup-linux-x64-gnu@4.62.4':
|
| 430 |
resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==}
|
| 431 |
cpu: [x64]
|
| 432 |
os: [linux]
|
|
|
|
| 433 |
|
| 434 |
'@rollup/rollup-linux-x64-musl@4.62.4':
|
| 435 |
resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==}
|
| 436 |
cpu: [x64]
|
| 437 |
os: [linux]
|
|
|
|
| 438 |
|
| 439 |
'@rollup/rollup-openbsd-x64@4.62.4':
|
| 440 |
resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==}
|
|
@@ -1162,6 +1194,12 @@ snapshots:
|
|
| 1162 |
'@esbuild/win32-x64@0.25.12':
|
| 1163 |
optional: true
|
| 1164 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1165 |
'@jridgewell/gen-mapping@0.3.13':
|
| 1166 |
dependencies:
|
| 1167 |
'@jridgewell/sourcemap-codec': 1.5.5
|
|
|
|
| 8 |
|
| 9 |
.:
|
| 10 |
dependencies:
|
| 11 |
+
'@fontsource-variable/inter':
|
| 12 |
+
specifier: ^5.3.0
|
| 13 |
+
version: 5.3.0
|
| 14 |
+
'@fontsource-variable/space-grotesk':
|
| 15 |
+
specifier: ^5.3.0
|
| 16 |
+
version: 5.3.0
|
| 17 |
+
'@fontsource/ibm-plex-mono':
|
| 18 |
+
specifier: ^5.3.0
|
| 19 |
+
version: 5.3.0
|
| 20 |
'@tanstack/react-query':
|
| 21 |
specifier: ^5.62.7
|
| 22 |
version: 5.101.4(react@18.3.1)
|
|
|
|
| 309 |
cpu: [x64]
|
| 310 |
os: [win32]
|
| 311 |
|
| 312 |
+
'@fontsource-variable/inter@5.3.0':
|
| 313 |
+
resolution: {integrity: sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==}
|
| 314 |
+
|
| 315 |
+
'@fontsource-variable/space-grotesk@5.3.0':
|
| 316 |
+
resolution: {integrity: sha512-2IxmvfB08i9vnGB3Ym/AXvhRE+8XOjWMXIyDum03c+tPwH0FUoMNQfGpU8NXPxjbws0Vvss3AH0Zqt4oJBBAdw==}
|
| 317 |
+
|
| 318 |
+
'@fontsource/ibm-plex-mono@5.3.0':
|
| 319 |
+
resolution: {integrity: sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q==}
|
| 320 |
+
|
| 321 |
'@jridgewell/gen-mapping@0.3.13':
|
| 322 |
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
| 323 |
|
|
|
|
| 339 |
engines: {node: ^22.20 || ^24.12 || >=25}
|
| 340 |
cpu: [x64]
|
| 341 |
os: [linux]
|
| 342 |
+
libc: [glibc]
|
| 343 |
|
| 344 |
'@nodelib/fs.scandir@2.1.5':
|
| 345 |
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
|
|
|
| 394 |
resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==}
|
| 395 |
cpu: [arm]
|
| 396 |
os: [linux]
|
| 397 |
+
libc: [glibc]
|
| 398 |
|
| 399 |
'@rollup/rollup-linux-arm-musleabihf@4.62.4':
|
| 400 |
resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==}
|
| 401 |
cpu: [arm]
|
| 402 |
os: [linux]
|
| 403 |
+
libc: [musl]
|
| 404 |
|
| 405 |
'@rollup/rollup-linux-arm64-gnu@4.62.4':
|
| 406 |
resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==}
|
| 407 |
cpu: [arm64]
|
| 408 |
os: [linux]
|
| 409 |
+
libc: [glibc]
|
| 410 |
|
| 411 |
'@rollup/rollup-linux-arm64-musl@4.62.4':
|
| 412 |
resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==}
|
| 413 |
cpu: [arm64]
|
| 414 |
os: [linux]
|
| 415 |
+
libc: [musl]
|
| 416 |
|
| 417 |
'@rollup/rollup-linux-loong64-gnu@4.62.4':
|
| 418 |
resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==}
|
| 419 |
cpu: [loong64]
|
| 420 |
os: [linux]
|
| 421 |
+
libc: [glibc]
|
| 422 |
|
| 423 |
'@rollup/rollup-linux-loong64-musl@4.62.4':
|
| 424 |
resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==}
|
| 425 |
cpu: [loong64]
|
| 426 |
os: [linux]
|
| 427 |
+
libc: [musl]
|
| 428 |
|
| 429 |
'@rollup/rollup-linux-ppc64-gnu@4.62.4':
|
| 430 |
resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==}
|
| 431 |
cpu: [ppc64]
|
| 432 |
os: [linux]
|
| 433 |
+
libc: [glibc]
|
| 434 |
|
| 435 |
'@rollup/rollup-linux-ppc64-musl@4.62.4':
|
| 436 |
resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==}
|
| 437 |
cpu: [ppc64]
|
| 438 |
os: [linux]
|
| 439 |
+
libc: [musl]
|
| 440 |
|
| 441 |
'@rollup/rollup-linux-riscv64-gnu@4.62.4':
|
| 442 |
resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==}
|
| 443 |
cpu: [riscv64]
|
| 444 |
os: [linux]
|
| 445 |
+
libc: [glibc]
|
| 446 |
|
| 447 |
'@rollup/rollup-linux-riscv64-musl@4.62.4':
|
| 448 |
resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==}
|
| 449 |
cpu: [riscv64]
|
| 450 |
os: [linux]
|
| 451 |
+
libc: [musl]
|
| 452 |
|
| 453 |
'@rollup/rollup-linux-s390x-gnu@4.62.4':
|
| 454 |
resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==}
|
| 455 |
cpu: [s390x]
|
| 456 |
os: [linux]
|
| 457 |
+
libc: [glibc]
|
| 458 |
|
| 459 |
'@rollup/rollup-linux-x64-gnu@4.62.4':
|
| 460 |
resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==}
|
| 461 |
cpu: [x64]
|
| 462 |
os: [linux]
|
| 463 |
+
libc: [glibc]
|
| 464 |
|
| 465 |
'@rollup/rollup-linux-x64-musl@4.62.4':
|
| 466 |
resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==}
|
| 467 |
cpu: [x64]
|
| 468 |
os: [linux]
|
| 469 |
+
libc: [musl]
|
| 470 |
|
| 471 |
'@rollup/rollup-openbsd-x64@4.62.4':
|
| 472 |
resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==}
|
|
|
|
| 1194 |
'@esbuild/win32-x64@0.25.12':
|
| 1195 |
optional: true
|
| 1196 |
|
| 1197 |
+
'@fontsource-variable/inter@5.3.0': {}
|
| 1198 |
+
|
| 1199 |
+
'@fontsource-variable/space-grotesk@5.3.0': {}
|
| 1200 |
+
|
| 1201 |
+
'@fontsource/ibm-plex-mono@5.3.0': {}
|
| 1202 |
+
|
| 1203 |
'@jridgewell/gen-mapping@0.3.13':
|
| 1204 |
dependencies:
|
| 1205 |
'@jridgewell/sourcemap-codec': 1.5.5
|
public/hero-bg-poster.jpg
ADDED
|
public/hero-bg.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:22578443c1f297aa6c9271d939f33ab8043bf98deb87f8c75b8a38948d11ee9d
|
| 3 |
+
size 1142870
|
public/logo.svg
ADDED
|
|
src/components/AuthButton.tsx
CHANGED
|
@@ -1,17 +1,17 @@
|
|
| 1 |
import { useEffect, useRef, useState } from "react";
|
| 2 |
-
import {
|
| 3 |
import { Github, LogOut, ChevronDown, FolderGit2 } from "lucide-react";
|
| 4 |
-
import { navigateToLogin, navigateToLoginPopup, isEmbedded
|
| 5 |
import { useLogout, useMe } from "../features/auth/useAuth";
|
| 6 |
|
| 7 |
/**
|
| 8 |
* 顶栏登录态:未登录 = Sign in with GitHub;已登录 = 头像 + 下拉(Logout)。
|
| 9 |
-
*
|
| 10 |
*/
|
| 11 |
export function AuthButton({ appearance = "light" }: { appearance?: "light" | "dark" }) {
|
|
|
|
| 12 |
const meQ = useMe();
|
| 13 |
const logoutM = useLogout();
|
| 14 |
-
const { pathname, search } = useLocation();
|
| 15 |
const nav = useNavigate();
|
| 16 |
const [open, setOpen] = useState(false);
|
| 17 |
const rootRef = useRef<HTMLDivElement>(null);
|
|
@@ -34,10 +34,6 @@ export function AuthButton({ appearance = "light" }: { appearance?: "light" | "d
|
|
| 34 |
|
| 35 |
// 加载中/后端未接入 auth:按未登录渲染(不阻塞页面)
|
| 36 |
const user = meQ.data?.authenticated ? meQ.data.user : null;
|
| 37 |
-
const dark = appearance === "dark";
|
| 38 |
-
// loginUrl for href fallback (non-iframe); navigateToLogin for onClick
|
| 39 |
-
void pathname; void search;
|
| 40 |
-
|
| 41 |
if (!user) {
|
| 42 |
return (
|
| 43 |
<button
|
|
@@ -47,16 +43,12 @@ export function AuthButton({ appearance = "light" }: { appearance?: "light" | "d
|
|
| 47 |
navigateToLoginPopup();
|
| 48 |
window.dispatchEvent(new Event("ov-oauth-popup-opened"));
|
| 49 |
} else {
|
| 50 |
-
navigateToLogin(
|
| 51 |
}
|
| 52 |
}}
|
| 53 |
-
className=
|
| 54 |
-
dark
|
| 55 |
-
? "inline-flex h-9 items-center gap-2 rounded-full border border-[#333] bg-[#030303] px-3.5 text-xs font-medium text-[#acacb0] transition hover:border-[#484a58] hover:bg-[#111216] hover:text-white focus-ring-dark"
|
| 56 |
-
: "inline-flex h-9 items-center gap-1.5 rounded-md border border-line bg-surface px-3 text-[13px] font-medium text-ink transition-colors hover:bg-surface-sunken focus-ring"
|
| 57 |
-
}
|
| 58 |
>
|
| 59 |
-
<Github size={
|
| 60 |
Sign in
|
| 61 |
</button>
|
| 62 |
);
|
|
@@ -70,11 +62,7 @@ export function AuthButton({ appearance = "light" }: { appearance?: "light" | "d
|
|
| 70 |
aria-expanded={open}
|
| 71 |
aria-haspopup="menu"
|
| 72 |
aria-label={`Signed in as ${user.login}`}
|
| 73 |
-
className=
|
| 74 |
-
dark
|
| 75 |
-
? "flex h-9 items-center gap-1.5 rounded-full border border-[#333] bg-[#030303] p-1 pr-2 transition hover:border-[#484a58] hover:bg-[#111216] focus-ring-dark"
|
| 76 |
-
: "flex h-9 items-center gap-1 rounded-md border border-line bg-surface p-1 pr-1.5 transition-colors hover:bg-surface-sunken focus-ring"
|
| 77 |
-
}
|
| 78 |
>
|
| 79 |
{user.avatar_url ? (
|
| 80 |
<img
|
|
@@ -90,22 +78,16 @@ export function AuthButton({ appearance = "light" }: { appearance?: "light" | "d
|
|
| 90 |
)}
|
| 91 |
<ChevronDown
|
| 92 |
size={13}
|
| 93 |
-
className={`
|
| 94 |
/>
|
| 95 |
</button>
|
| 96 |
|
| 97 |
{open && (
|
| 98 |
<div
|
| 99 |
role="menu"
|
| 100 |
-
className=
|
| 101 |
-
dark
|
| 102 |
-
? "absolute right-0 top-full z-40 mt-2 w-44 overflow-hidden rounded-xl border border-[#333] bg-[#111216] shadow-[0_16px_50px_rgba(0,0,0,0.5)]"
|
| 103 |
-
: "absolute right-0 top-full z-40 mt-1.5 w-44 overflow-hidden rounded-lg border border-line bg-surface shadow-lg"
|
| 104 |
-
}
|
| 105 |
>
|
| 106 |
-
<div
|
| 107 |
-
className={`border-b px-3.5 py-2.5 text-[13px] font-medium ${dark ? "border-[#26272c] text-[#f0f2f6]" : "border-line text-ink"}`}
|
| 108 |
-
>
|
| 109 |
{user.login}
|
| 110 |
</div>
|
| 111 |
<a
|
|
@@ -116,11 +98,7 @@ export function AuthButton({ appearance = "light" }: { appearance?: "light" | "d
|
|
| 116 |
setOpen(false);
|
| 117 |
nav("/my");
|
| 118 |
}}
|
| 119 |
-
className=
|
| 120 |
-
dark
|
| 121 |
-
? "text-[#acacb0] hover:bg-[#1a1b20] hover:text-white"
|
| 122 |
-
: "text-ink-secondary hover:bg-surface-sunken hover:text-ink"
|
| 123 |
-
}`}
|
| 124 |
>
|
| 125 |
<FolderGit2 size={14} />
|
| 126 |
My submissions
|
|
@@ -133,11 +111,7 @@ export function AuthButton({ appearance = "light" }: { appearance?: "light" | "d
|
|
| 133 |
logoutM.mutate();
|
| 134 |
}}
|
| 135 |
disabled={logoutM.isPending}
|
| 136 |
-
className=
|
| 137 |
-
dark
|
| 138 |
-
? "text-[#acacb0] hover:bg-[#1a1b20] hover:text-white"
|
| 139 |
-
: "text-ink-secondary hover:bg-surface-sunken hover:text-ink"
|
| 140 |
-
}`}
|
| 141 |
>
|
| 142 |
<LogOut size={14} />
|
| 143 |
{logoutM.isPending ? "Signing out…" : "Sign out"}
|
|
|
|
| 1 |
import { useEffect, useRef, useState } from "react";
|
| 2 |
+
import { useNavigate } from "react-router-dom";
|
| 3 |
import { Github, LogOut, ChevronDown, FolderGit2 } from "lucide-react";
|
| 4 |
+
import { navigateToLogin, navigateToLoginPopup, isEmbedded } from "../shared/api/client";
|
| 5 |
import { useLogout, useMe } from "../features/auth/useAuth";
|
| 6 |
|
| 7 |
/**
|
| 8 |
* 顶栏登录态:未登录 = Sign in with GitHub;已登录 = 头像 + 下拉(Logout)。
|
| 9 |
+
* 浅色体系统一样式;appearance prop 仅为调用方兼容保留。
|
| 10 |
*/
|
| 11 |
export function AuthButton({ appearance = "light" }: { appearance?: "light" | "dark" }) {
|
| 12 |
+
void appearance;
|
| 13 |
const meQ = useMe();
|
| 14 |
const logoutM = useLogout();
|
|
|
|
| 15 |
const nav = useNavigate();
|
| 16 |
const [open, setOpen] = useState(false);
|
| 17 |
const rootRef = useRef<HTMLDivElement>(null);
|
|
|
|
| 34 |
|
| 35 |
// 加载中/后端未接入 auth:按未登录渲染(不阻塞页面)
|
| 36 |
const user = meQ.data?.authenticated ? meQ.data.user : null;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
if (!user) {
|
| 38 |
return (
|
| 39 |
<button
|
|
|
|
| 43 |
navigateToLoginPopup();
|
| 44 |
window.dispatchEvent(new Event("ov-oauth-popup-opened"));
|
| 45 |
} else {
|
| 46 |
+
navigateToLogin();
|
| 47 |
}
|
| 48 |
}}
|
| 49 |
+
className="inline-flex h-9 items-center gap-2 rounded-full bg-ink px-4 text-[13px] font-medium text-surface transition hover:bg-white focus-ring"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
>
|
| 51 |
+
<Github size={14} />
|
| 52 |
Sign in
|
| 53 |
</button>
|
| 54 |
);
|
|
|
|
| 62 |
aria-expanded={open}
|
| 63 |
aria-haspopup="menu"
|
| 64 |
aria-label={`Signed in as ${user.login}`}
|
| 65 |
+
className="flex h-9 items-center gap-1.5 rounded-full border border-line bg-surface-raised p-1 pr-2 transition-colors hover:border-line-strong hover:bg-surface-sunken focus-ring"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
>
|
| 67 |
{user.avatar_url ? (
|
| 68 |
<img
|
|
|
|
| 78 |
)}
|
| 79 |
<ChevronDown
|
| 80 |
size={13}
|
| 81 |
+
className={`text-ink-tertiary transition-transform ${open ? "rotate-180" : ""}`}
|
| 82 |
/>
|
| 83 |
</button>
|
| 84 |
|
| 85 |
{open && (
|
| 86 |
<div
|
| 87 |
role="menu"
|
| 88 |
+
className="absolute right-0 top-full z-40 mt-2 w-44 overflow-hidden rounded-xl border border-line bg-surface-raised shadow-[0_16px_48px_-12px_rgba(0,0,0,0.7)]"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
>
|
| 90 |
+
<div className="border-b border-line px-3.5 py-2.5 text-[13px] font-medium text-ink">
|
|
|
|
|
|
|
| 91 |
{user.login}
|
| 92 |
</div>
|
| 93 |
<a
|
|
|
|
| 98 |
setOpen(false);
|
| 99 |
nav("/my");
|
| 100 |
}}
|
| 101 |
+
className="flex w-full items-center gap-2 px-3.5 py-2.5 text-left text-[13px] text-ink-secondary transition-colors hover:bg-surface-sunken hover:text-ink"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
>
|
| 103 |
<FolderGit2 size={14} />
|
| 104 |
My submissions
|
|
|
|
| 111 |
logoutM.mutate();
|
| 112 |
}}
|
| 113 |
disabled={logoutM.isPending}
|
| 114 |
+
className="flex w-full items-center gap-2 px-3.5 py-2.5 text-left text-[13px] text-ink-secondary transition-colors hover:bg-surface-sunken hover:text-ink"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
>
|
| 116 |
<LogOut size={14} />
|
| 117 |
{logoutM.isPending ? "Signing out…" : "Sign out"}
|
src/components/Button.tsx
CHANGED
|
@@ -4,11 +4,11 @@ type Variant = "primary" | "secondary" | "ghost" | "danger";
|
|
| 4 |
type Size = "md" | "lg";
|
| 5 |
|
| 6 |
const styles: Record<Variant, string> = {
|
| 7 |
-
//
|
| 8 |
-
primary: "bg-
|
| 9 |
-
secondary: "bg-surface-raised text-ink border border-
|
| 10 |
ghost: "bg-transparent text-ink-secondary border border-transparent hover:bg-surface-sunken hover:text-ink",
|
| 11 |
-
danger: "bg-danger text-
|
| 12 |
};
|
| 13 |
|
| 14 |
// 高度集中在 size prop,禁止 className 覆盖高度(Tailwind 同源类冲突靠 CSS 顺序,不可靠)
|
|
|
|
| 4 |
type Size = "md" | "lg";
|
| 5 |
|
| 6 |
const styles: Record<Variant, string> = {
|
| 7 |
+
// 暗色控制台:主按钮 = 亮面(phosphor 白底 + 暗字)
|
| 8 |
+
primary: "bg-ink text-surface hover:bg-white border border-transparent",
|
| 9 |
+
secondary: "bg-surface-raised text-ink border border-line hover:border-line-strong hover:bg-surface-sunken",
|
| 10 |
ghost: "bg-transparent text-ink-secondary border border-transparent hover:bg-surface-sunken hover:text-ink",
|
| 11 |
+
danger: "bg-danger text-surface hover:bg-[#ff7d77] border border-transparent",
|
| 12 |
};
|
| 13 |
|
| 14 |
// 高度集中在 size prop,禁止 className 覆盖高度(Tailwind 同源类冲突靠 CSS 顺序,不可靠)
|
src/components/ConfirmDialog.tsx
CHANGED
|
@@ -36,7 +36,7 @@ export function ConfirmDialog({
|
|
| 36 |
|
| 37 |
return (
|
| 38 |
<div
|
| 39 |
-
className="fixed inset-0 z-50 flex items-center justify-center bg-
|
| 40 |
role="dialog"
|
| 41 |
aria-modal="true"
|
| 42 |
aria-labelledby="confirm-title"
|
|
|
|
| 36 |
|
| 37 |
return (
|
| 38 |
<div
|
| 39 |
+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4"
|
| 40 |
role="dialog"
|
| 41 |
aria-modal="true"
|
| 42 |
aria-labelledby="confirm-title"
|
src/components/HeroStats.tsx
DELETED
|
@@ -1,115 +0,0 @@
|
|
| 1 |
-
import { useEffect, useState } from "react";
|
| 2 |
-
import { AlertTriangle, Bug, FolderGit2 } from "lucide-react";
|
| 3 |
-
import type { OverviewStats } from "@/shared/types";
|
| 4 |
-
|
| 5 |
-
function usePrefersReducedMotion(): boolean {
|
| 6 |
-
const [reduced, setReduced] = useState(false);
|
| 7 |
-
useEffect(() => {
|
| 8 |
-
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
|
| 9 |
-
setReduced(media.matches);
|
| 10 |
-
const update = () => setReduced(media.matches);
|
| 11 |
-
media.addEventListener("change", update);
|
| 12 |
-
return () => media.removeEventListener("change", update);
|
| 13 |
-
}, []);
|
| 14 |
-
return reduced;
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
function useCountUp(target: number, duration = 650): number {
|
| 18 |
-
const reduced = usePrefersReducedMotion();
|
| 19 |
-
const [value, setValue] = useState(reduced ? target : 0);
|
| 20 |
-
useEffect(() => {
|
| 21 |
-
if (reduced) {
|
| 22 |
-
setValue(target);
|
| 23 |
-
return;
|
| 24 |
-
}
|
| 25 |
-
let frame = 0;
|
| 26 |
-
const start = performance.now();
|
| 27 |
-
const tick = (time: number) => {
|
| 28 |
-
const progress = Math.min(1, (time - start) / duration);
|
| 29 |
-
setValue(Math.round(target * (1 - (1 - progress) ** 3)));
|
| 30 |
-
if (progress < 1) frame = requestAnimationFrame(tick);
|
| 31 |
-
};
|
| 32 |
-
frame = requestAnimationFrame(tick);
|
| 33 |
-
return () => cancelAnimationFrame(frame);
|
| 34 |
-
}, [duration, reduced, target]);
|
| 35 |
-
return value;
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
function AnimatedNumber({ value }: { value: number }) {
|
| 39 |
-
return <>{useCountUp(value).toLocaleString()}</>;
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
export function HeroStats({
|
| 43 |
-
stats,
|
| 44 |
-
loading = false,
|
| 45 |
-
failed = false,
|
| 46 |
-
}: {
|
| 47 |
-
stats: OverviewStats | undefined;
|
| 48 |
-
loading?: boolean;
|
| 49 |
-
failed?: boolean;
|
| 50 |
-
}) {
|
| 51 |
-
const unavailable = loading || failed || !stats;
|
| 52 |
-
const scannedRepositories = stats
|
| 53 |
-
? stats.scanned_project_count ?? Math.min(stats.project_count, stats.scan_completed_count)
|
| 54 |
-
: 0;
|
| 55 |
-
const findingTotal = stats?.finding_total ?? 0;
|
| 56 |
-
const highRiskTotal = stats?.severity_counts?.high ?? 0;
|
| 57 |
-
|
| 58 |
-
const metrics = [
|
| 59 |
-
{
|
| 60 |
-
label: "Findings",
|
| 61 |
-
detail: "Discovered issues",
|
| 62 |
-
value: findingTotal,
|
| 63 |
-
icon: Bug,
|
| 64 |
-
color: "text-white",
|
| 65 |
-
iconClass: "border-white/[0.08] bg-white/[0.05] text-[#b5b6bd]",
|
| 66 |
-
},
|
| 67 |
-
{
|
| 68 |
-
label: "High risk",
|
| 69 |
-
detail: "High severity findings",
|
| 70 |
-
value: highRiskTotal,
|
| 71 |
-
icon: AlertTriangle,
|
| 72 |
-
color: "text-white",
|
| 73 |
-
iconClass: "border-white/[0.08] bg-white/[0.05] text-[#b5b6bd]",
|
| 74 |
-
},
|
| 75 |
-
{
|
| 76 |
-
label: "Repositories",
|
| 77 |
-
detail: "Scanned projects",
|
| 78 |
-
value: scannedRepositories,
|
| 79 |
-
icon: FolderGit2,
|
| 80 |
-
color: "text-white",
|
| 81 |
-
iconClass: "border-white/[0.08] bg-white/[0.05] text-[#b5b6bd]",
|
| 82 |
-
},
|
| 83 |
-
];
|
| 84 |
-
|
| 85 |
-
return (
|
| 86 |
-
<section aria-labelledby="overview-title" className="openvuln-panel min-w-0 p-5 sm:p-6">
|
| 87 |
-
<div className="flex items-end justify-between gap-4">
|
| 88 |
-
<div>
|
| 89 |
-
<p className="font-mono text-[9px] font-semibold uppercase tracking-[0.18em] text-ink-tertiary">
|
| 90 |
-
Security overview
|
| 91 |
-
</p>
|
| 92 |
-
<h2 id="overview-title" className="mt-2 font-display text-lg font-semibold tracking-tight text-white">
|
| 93 |
-
Findings at a glance
|
| 94 |
-
</h2>
|
| 95 |
-
</div>
|
| 96 |
-
<span className="mb-1 h-2 w-2 rounded-full bg-white/60" aria-hidden />
|
| 97 |
-
</div>
|
| 98 |
-
|
| 99 |
-
<div className="mt-5 grid grid-cols-3 divide-x divide-white/[0.07]">
|
| 100 |
-
{metrics.map(({ label, detail, value, icon: Icon, color, iconClass }) => (
|
| 101 |
-
<div key={label} className="min-w-0 px-3 first:pl-0 last:pr-0 sm:px-5">
|
| 102 |
-
<div className={`flex h-8 w-8 items-center justify-center rounded-lg border ${iconClass}`} aria-hidden>
|
| 103 |
-
<Icon size={15} />
|
| 104 |
-
</div>
|
| 105 |
-
<p className={`mt-4 font-display text-3xl font-semibold tabular-nums tracking-[-0.04em] sm:text-4xl ${color}`}>
|
| 106 |
-
{unavailable ? "—" : <AnimatedNumber value={value} />}
|
| 107 |
-
</p>
|
| 108 |
-
<p className="mt-2 truncate text-[11px] font-medium text-[#d0d1d5] sm:text-xs">{label}</p>
|
| 109 |
-
<p className="mt-0.5 hidden text-[10px] leading-4 text-ink-tertiary sm:block">{detail}</p>
|
| 110 |
-
</div>
|
| 111 |
-
))}
|
| 112 |
-
</div>
|
| 113 |
-
</section>
|
| 114 |
-
);
|
| 115 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/components/Layout.tsx
CHANGED
|
@@ -13,28 +13,36 @@ export function Layout() {
|
|
| 13 |
const isDeck = pathname === "/";
|
| 14 |
return (
|
| 15 |
<div className="flex min-h-screen flex-col">
|
| 16 |
-
{!isDeck && <header className="app-shell-chrome sticky top-0 z-30 h-14 border-b border-line bg-surface
|
| 17 |
<div className="flex h-full items-center justify-between gap-4 px-5">
|
| 18 |
<div className="flex items-center">
|
| 19 |
<Link
|
| 20 |
to="/"
|
| 21 |
-
className="
|
| 22 |
>
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
</Link>
|
| 25 |
</div>
|
| 26 |
|
| 27 |
<div className="flex items-center gap-2">
|
| 28 |
-
<NotificationBell
|
| 29 |
-
<AuthButton
|
| 30 |
<a
|
| 31 |
href={OWN_REPO}
|
| 32 |
target="_blank"
|
| 33 |
rel="noreferrer"
|
| 34 |
-
className="rounded-
|
| 35 |
title="OpenVuln on GitHub"
|
| 36 |
>
|
| 37 |
-
<Github size={
|
| 38 |
</a>
|
| 39 |
</div>
|
| 40 |
</div>
|
|
@@ -46,7 +54,8 @@ export function Layout() {
|
|
| 46 |
|
| 47 |
{!isDeck && (
|
| 48 |
<footer className="app-shell-chrome mt-auto border-t border-line">
|
| 49 |
-
<div className="flex
|
|
|
|
| 50 |
<div className="flex flex-wrap gap-x-5 gap-y-1">
|
| 51 |
<a href={OWN_REPO} target="_blank" rel="noreferrer" className="hover:text-ink">
|
| 52 |
GitHub
|
|
|
|
| 13 |
const isDeck = pathname === "/";
|
| 14 |
return (
|
| 15 |
<div className="flex min-h-screen flex-col">
|
| 16 |
+
{!isDeck && <header className="app-shell-chrome sticky top-0 z-30 h-14 border-b border-line bg-surface/85 backdrop-blur">
|
| 17 |
<div className="flex h-full items-center justify-between gap-4 px-5">
|
| 18 |
<div className="flex items-center">
|
| 19 |
<Link
|
| 20 |
to="/"
|
| 21 |
+
className="flex items-center gap-2.5 rounded-md focus-ring"
|
| 22 |
>
|
| 23 |
+
<img
|
| 24 |
+
src="/logo.svg"
|
| 25 |
+
alt=""
|
| 26 |
+
className="h-7 w-7 shrink-0 rounded-lg"
|
| 27 |
+
aria-hidden
|
| 28 |
+
/>
|
| 29 |
+
<span className="font-display text-[15px] font-semibold tracking-tight text-ink">
|
| 30 |
+
OpenVuln
|
| 31 |
+
</span>
|
| 32 |
</Link>
|
| 33 |
</div>
|
| 34 |
|
| 35 |
<div className="flex items-center gap-2">
|
| 36 |
+
<NotificationBell />
|
| 37 |
+
<AuthButton />
|
| 38 |
<a
|
| 39 |
href={OWN_REPO}
|
| 40 |
target="_blank"
|
| 41 |
rel="noreferrer"
|
| 42 |
+
className="flex h-9 w-9 items-center justify-center rounded-full border border-line bg-surface-raised text-ink-secondary transition-colors hover:border-line-strong hover:bg-surface-sunken hover:text-ink focus-ring"
|
| 43 |
title="OpenVuln on GitHub"
|
| 44 |
>
|
| 45 |
+
<Github size={15} />
|
| 46 |
</a>
|
| 47 |
</div>
|
| 48 |
</div>
|
|
|
|
| 54 |
|
| 55 |
{!isDeck && (
|
| 56 |
<footer className="app-shell-chrome mt-auto border-t border-line">
|
| 57 |
+
<div className="flex flex-col gap-2 px-5 py-6 text-[13px] text-ink-secondary sm:flex-row sm:items-center sm:justify-between">
|
| 58 |
+
<p className="text-ink-tertiary">© 2026 OpenVuln · Powered by VulnHunter</p>
|
| 59 |
<div className="flex flex-wrap gap-x-5 gap-y-1">
|
| 60 |
<a href={OWN_REPO} target="_blank" rel="noreferrer" className="hover:text-ink">
|
| 61 |
GitHub
|
src/components/NotificationBell.tsx
CHANGED
|
@@ -21,6 +21,7 @@ function summary(n: NotificationItem): string {
|
|
| 21 |
|
| 22 |
/** 站内通知铃铛(task-78c9fb3a):未读红点 + 下拉列表,点击跳项目页并标已读。60s 轮询。 */
|
| 23 |
export function NotificationBell({ appearance = "light" }: { appearance?: "light" | "dark" }) {
|
|
|
|
| 24 |
const meQ = useMe();
|
| 25 |
const authed = meQ.data?.authenticated === true;
|
| 26 |
const nav = useNavigate();
|
|
@@ -59,7 +60,6 @@ export function NotificationBell({ appearance = "light" }: { appearance?: "light
|
|
| 59 |
|
| 60 |
if (!authed) return null;
|
| 61 |
|
| 62 |
-
const dark = appearance === "dark";
|
| 63 |
const items = listQ.data?.notifications ?? [];
|
| 64 |
const unread = listQ.data?.unread_count ?? 0;
|
| 65 |
|
|
@@ -78,15 +78,11 @@ export function NotificationBell({ appearance = "light" }: { appearance?: "light
|
|
| 78 |
aria-expanded={open}
|
| 79 |
aria-haspopup="menu"
|
| 80 |
aria-label={unread > 0 ? `Notifications (${unread} unread)` : "Notifications"}
|
| 81 |
-
className=
|
| 82 |
-
dark
|
| 83 |
-
? "relative flex h-9 w-9 items-center justify-center rounded-full border border-[#333] bg-[#030303] text-[#acacb0] transition hover:border-[#484a58] hover:bg-[#111216] hover:text-white focus-ring-dark"
|
| 84 |
-
: "relative flex h-9 w-9 items-center justify-center rounded-md border border-line bg-surface text-ink-secondary transition-colors hover:bg-surface-sunken hover:text-ink focus-ring"
|
| 85 |
-
}
|
| 86 |
>
|
| 87 |
-
<Bell size={
|
| 88 |
{unread > 0 && (
|
| 89 |
-
|
| 90 |
{unread > 9 ? "9+" : unread}
|
| 91 |
</span>
|
| 92 |
)}
|
|
@@ -95,24 +91,16 @@ export function NotificationBell({ appearance = "light" }: { appearance?: "light
|
|
| 95 |
{open && (
|
| 96 |
<div
|
| 97 |
role="menu"
|
| 98 |
-
className=
|
| 99 |
-
dark
|
| 100 |
-
? "absolute right-0 top-full z-40 mt-2 w-80 overflow-hidden rounded-xl border border-[#333] bg-[#111216] shadow-[0_16px_50px_rgba(0,0,0,0.5)]"
|
| 101 |
-
: "absolute right-0 top-full z-40 mt-1.5 w-80 overflow-hidden rounded-lg border border-line bg-surface shadow-lg"
|
| 102 |
-
}
|
| 103 |
>
|
| 104 |
-
<div
|
| 105 |
-
className=
|
| 106 |
-
>
|
| 107 |
-
<span className={`text-[13px] font-medium ${dark ? "text-[#f0f2f6]" : "text-ink"}`}>
|
| 108 |
-
Notifications
|
| 109 |
-
</span>
|
| 110 |
{unread > 0 && (
|
| 111 |
<button
|
| 112 |
type="button"
|
| 113 |
onClick={() => readAllM.mutate()}
|
| 114 |
disabled={readAllM.isPending}
|
| 115 |
-
className=
|
| 116 |
>
|
| 117 |
<CheckCheck size={13} />
|
| 118 |
Mark all read
|
|
@@ -122,11 +110,9 @@ export function NotificationBell({ appearance = "light" }: { appearance?: "light
|
|
| 122 |
|
| 123 |
<ul className="max-h-80 overflow-y-auto">
|
| 124 |
{listQ.isPending ? (
|
| 125 |
-
<li className=
|
| 126 |
-
Loading…
|
| 127 |
-
</li>
|
| 128 |
) : items.length === 0 ? (
|
| 129 |
-
<li className=
|
| 130 |
No notifications yet
|
| 131 |
</li>
|
| 132 |
) : (
|
|
@@ -138,22 +124,20 @@ export function NotificationBell({ appearance = "light" }: { appearance?: "light
|
|
| 138 |
type="button"
|
| 139 |
role="menuitem"
|
| 140 |
onClick={() => openItem(n)}
|
| 141 |
-
className=
|
| 142 |
-
dark ? "hover:bg-[#1a1b20]" : "hover:bg-surface-sunken"
|
| 143 |
-
}`}
|
| 144 |
>
|
| 145 |
<span
|
| 146 |
className={`mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full ${isUnread ? "bg-accent-600" : "bg-transparent"}`}
|
| 147 |
aria-hidden
|
| 148 |
/>
|
| 149 |
<span className="min-w-0 flex-1">
|
| 150 |
-
<span className=
|
| 151 |
{n.payload.full_name}
|
| 152 |
</span>
|
| 153 |
-
<span className=
|
| 154 |
{summary(n)}
|
| 155 |
</span>
|
| 156 |
-
<span className=
|
| 157 |
{formatRelativeTime(n.created_at)}
|
| 158 |
</span>
|
| 159 |
</span>
|
|
|
|
| 21 |
|
| 22 |
/** 站内通知铃铛(task-78c9fb3a):未读红点 + 下拉列表,点击跳项目页并标已读。60s 轮询。 */
|
| 23 |
export function NotificationBell({ appearance = "light" }: { appearance?: "light" | "dark" }) {
|
| 24 |
+
void appearance;
|
| 25 |
const meQ = useMe();
|
| 26 |
const authed = meQ.data?.authenticated === true;
|
| 27 |
const nav = useNavigate();
|
|
|
|
| 60 |
|
| 61 |
if (!authed) return null;
|
| 62 |
|
|
|
|
| 63 |
const items = listQ.data?.notifications ?? [];
|
| 64 |
const unread = listQ.data?.unread_count ?? 0;
|
| 65 |
|
|
|
|
| 78 |
aria-expanded={open}
|
| 79 |
aria-haspopup="menu"
|
| 80 |
aria-label={unread > 0 ? `Notifications (${unread} unread)` : "Notifications"}
|
| 81 |
+
className="relative flex h-9 w-9 items-center justify-center rounded-full border border-line bg-surface-raised text-ink-secondary transition-colors hover:border-line-strong hover:bg-surface-sunken hover:text-ink focus-ring"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
>
|
| 83 |
+
<Bell size={15} />
|
| 84 |
{unread > 0 && (
|
| 85 |
+
<span className="absolute -right-1 -top-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-danger px-1 text-[10px] font-semibold leading-none text-surface">
|
| 86 |
{unread > 9 ? "9+" : unread}
|
| 87 |
</span>
|
| 88 |
)}
|
|
|
|
| 91 |
{open && (
|
| 92 |
<div
|
| 93 |
role="menu"
|
| 94 |
+
className="absolute right-0 top-full z-40 mt-2 w-80 overflow-hidden rounded-xl border border-line bg-surface-raised shadow-[0_16px_48px_-12px_rgba(0,0,0,0.7)]"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
>
|
| 96 |
+
<div className="flex items-center justify-between border-b border-line px-3.5 py-2.5">
|
| 97 |
+
<span className="text-[13px] font-medium text-ink">Notifications</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
{unread > 0 && (
|
| 99 |
<button
|
| 100 |
type="button"
|
| 101 |
onClick={() => readAllM.mutate()}
|
| 102 |
disabled={readAllM.isPending}
|
| 103 |
+
className="inline-flex items-center gap-1 text-[12px] text-ink-secondary hover:text-ink"
|
| 104 |
>
|
| 105 |
<CheckCheck size={13} />
|
| 106 |
Mark all read
|
|
|
|
| 110 |
|
| 111 |
<ul className="max-h-80 overflow-y-auto">
|
| 112 |
{listQ.isPending ? (
|
| 113 |
+
<li className="px-3.5 py-6 text-center text-[13px] text-ink-tertiary">Loading…</li>
|
|
|
|
|
|
|
| 114 |
) : items.length === 0 ? (
|
| 115 |
+
<li className="px-3.5 py-6 text-center text-[13px] text-ink-tertiary">
|
| 116 |
No notifications yet
|
| 117 |
</li>
|
| 118 |
) : (
|
|
|
|
| 124 |
type="button"
|
| 125 |
role="menuitem"
|
| 126 |
onClick={() => openItem(n)}
|
| 127 |
+
className="flex w-full items-start gap-2.5 px-3.5 py-2.5 text-left transition-colors hover:bg-surface-sunken"
|
|
|
|
|
|
|
| 128 |
>
|
| 129 |
<span
|
| 130 |
className={`mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full ${isUnread ? "bg-accent-600" : "bg-transparent"}`}
|
| 131 |
aria-hidden
|
| 132 |
/>
|
| 133 |
<span className="min-w-0 flex-1">
|
| 134 |
+
<span className="block truncate text-[13px] font-medium text-ink">
|
| 135 |
{n.payload.full_name}
|
| 136 |
</span>
|
| 137 |
+
<span className="mt-0.5 block text-[12px] leading-snug text-ink-secondary">
|
| 138 |
{summary(n)}
|
| 139 |
</span>
|
| 140 |
+
<span className="mt-0.5 block font-mono text-[11px] text-ink-tertiary">
|
| 141 |
{formatRelativeTime(n.created_at)}
|
| 142 |
</span>
|
| 143 |
</span>
|
src/components/ProjectRow.tsx
CHANGED
|
@@ -3,7 +3,6 @@ import { useNavigate } from "react-router-dom";
|
|
| 3 |
import type { ProjectCard } from "@/shared/types";
|
| 4 |
import { formatRelativeTime, formatStars, totalFindings } from "../shared/lib/format";
|
| 5 |
import { SeverityBar } from "./SeverityBar";
|
| 6 |
-
import { StatusBadge } from "./StatusBadge";
|
| 7 |
|
| 8 |
function OwnerAvatar({ owner, compact = false }: { owner: string; compact?: boolean }) {
|
| 9 |
const [failed, setFailed] = useState(false);
|
|
@@ -11,7 +10,7 @@ function OwnerAvatar({ owner, compact = false }: { owner: string; compact?: bool
|
|
| 11 |
if (failed) {
|
| 12 |
return (
|
| 13 |
<div
|
| 14 |
-
className={`flex shrink-0 items-center justify-center rounded-full bg-
|
| 15 |
aria-hidden
|
| 16 |
>
|
| 17 |
{owner.slice(0, 1).toUpperCase()}
|
|
@@ -46,7 +45,7 @@ export function ProjectRow({ project, compact = false }: { project: ProjectCard;
|
|
| 46 |
onKeyDown={(e) => {
|
| 47 |
if (e.key === "Enter") nav(`/p/${project.owner_login}/${project.name}`);
|
| 48 |
}}
|
| 49 |
-
className={`group block cursor-pointer overflow-hidden border-b border-line px-1 transition-colors hover:bg-
|
| 50 |
compact ? "h-[104px] py-3" : "py-4"
|
| 51 |
}`}
|
| 52 |
>
|
|
@@ -89,7 +88,13 @@ export function ProjectRow({ project, compact = false }: { project: ProjectCard;
|
|
| 89 |
</div>
|
| 90 |
<div className={compact ? "mt-1.5" : "mt-2.5"}>
|
| 91 |
{scanning ? (
|
| 92 |
-
<span
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
{soFar > 0 ? `Scanning · ${soFar} findings so far` : "Scanning…"}
|
| 94 |
</span>
|
| 95 |
) : waiting ? (
|
|
@@ -107,12 +112,6 @@ export function ProjectRow({ project, compact = false }: { project: ProjectCard;
|
|
| 107 |
)}
|
| 108 |
</div>
|
| 109 |
</div>
|
| 110 |
-
{/* completed 的扫描时间已在 meta 行,右侧 badge 只留给需要注意的状态(去重,fish v1.11) */}
|
| 111 |
-
{state !== "completed" && (
|
| 112 |
-
<div className={`shrink-0 pt-0.5 ${compact ? "scale-90 origin-top-right" : ""}`}>
|
| 113 |
-
<StatusBadge state={state} finishedAt={project.latest_scan?.finished_at} />
|
| 114 |
-
</div>
|
| 115 |
-
)}
|
| 116 |
</div>
|
| 117 |
</div>
|
| 118 |
</div>
|
|
|
|
| 3 |
import type { ProjectCard } from "@/shared/types";
|
| 4 |
import { formatRelativeTime, formatStars, totalFindings } from "../shared/lib/format";
|
| 5 |
import { SeverityBar } from "./SeverityBar";
|
|
|
|
| 6 |
|
| 7 |
function OwnerAvatar({ owner, compact = false }: { owner: string; compact?: boolean }) {
|
| 8 |
const [failed, setFailed] = useState(false);
|
|
|
|
| 10 |
if (failed) {
|
| 11 |
return (
|
| 12 |
<div
|
| 13 |
+
className={`flex shrink-0 items-center justify-center rounded-full bg-surface-sunken font-semibold text-ink-secondary ${sizeClass} ${compact ? "text-[10px]" : "text-xs"}`}
|
| 14 |
aria-hidden
|
| 15 |
>
|
| 16 |
{owner.slice(0, 1).toUpperCase()}
|
|
|
|
| 45 |
onKeyDown={(e) => {
|
| 46 |
if (e.key === "Enter") nav(`/p/${project.owner_login}/${project.name}`);
|
| 47 |
}}
|
| 48 |
+
className={`group block cursor-pointer overflow-hidden border-b border-line px-1 transition-colors hover:bg-surface-sunken/60 focus-ring rounded-sm ${
|
| 49 |
compact ? "h-[104px] py-3" : "py-4"
|
| 50 |
}`}
|
| 51 |
>
|
|
|
|
| 88 |
</div>
|
| 89 |
<div className={compact ? "mt-1.5" : "mt-2.5"}>
|
| 90 |
{scanning ? (
|
| 91 |
+
<span
|
| 92 |
+
className={`inline-flex items-center gap-1.5 font-medium text-running-ink ${compact ? "text-[10px]" : "text-[13px]"}`}
|
| 93 |
+
>
|
| 94 |
+
<span className="relative flex h-1.5 w-1.5">
|
| 95 |
+
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-running opacity-60 motion-reduce:animate-none" />
|
| 96 |
+
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-running" />
|
| 97 |
+
</span>
|
| 98 |
{soFar > 0 ? `Scanning · ${soFar} findings so far` : "Scanning…"}
|
| 99 |
</span>
|
| 100 |
) : waiting ? (
|
|
|
|
| 112 |
)}
|
| 113 |
</div>
|
| 114 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
</div>
|
| 116 |
</div>
|
| 117 |
</div>
|
src/components/RepoSubmitForm.tsx
CHANGED
|
@@ -49,10 +49,12 @@ export function RepoSubmitForm({
|
|
| 49 |
size = "default",
|
| 50 |
appearance = "default",
|
| 51 |
className = "",
|
|
|
|
| 52 |
}: {
|
| 53 |
size?: "default" | "hero";
|
| 54 |
appearance?: "default" | "dark";
|
| 55 |
className?: string;
|
|
|
|
| 56 |
}) {
|
| 57 |
const nav = useNavigate();
|
| 58 |
const meQ = useMe();
|
|
@@ -93,6 +95,8 @@ export function RepoSubmitForm({
|
|
| 93 |
};
|
| 94 |
|
| 95 |
if (dark) {
|
|
|
|
|
|
|
| 96 |
return (
|
| 97 |
<form
|
| 98 |
onSubmit={(e) => void onSubmit(e)}
|
|
@@ -101,13 +105,13 @@ export function RepoSubmitForm({
|
|
| 101 |
aria-busy={pending}
|
| 102 |
>
|
| 103 |
<div
|
| 104 |
-
className={`openvuln-composer group flex min-h-[
|
| 105 |
-
error ? "border-
|
| 106 |
}`}
|
| 107 |
>
|
| 108 |
<Github
|
| 109 |
-
size={
|
| 110 |
-
className="shrink-0 text-
|
| 111 |
/>
|
| 112 |
<input
|
| 113 |
type="url"
|
|
@@ -118,25 +122,25 @@ export function RepoSubmitForm({
|
|
| 118 |
}}
|
| 119 |
placeholder="Paste a public GitHub repository URL"
|
| 120 |
spellCheck={false}
|
| 121 |
-
className="min-w-0 flex-1 bg-transparent py-3 text-[
|
| 122 |
aria-label="GitHub repository URL"
|
| 123 |
aria-invalid={!!error}
|
| 124 |
/>
|
| 125 |
<button
|
| 126 |
type="submit"
|
| 127 |
aria-label={pending ? "Submitting repository" : "Analyze repository"}
|
| 128 |
-
className="flex h-
|
| 129 |
disabled={!url.trim() || pending}
|
| 130 |
>
|
| 131 |
{pending ? (
|
| 132 |
-
<LoaderCircle size={
|
| 133 |
) : (
|
| 134 |
-
<ArrowUp size={
|
| 135 |
)}
|
| 136 |
</button>
|
| 137 |
</div>
|
| 138 |
|
| 139 |
-
<div className=
|
| 140 |
{showRef ? (
|
| 141 |
<input
|
| 142 |
type="text"
|
|
@@ -144,28 +148,28 @@ export function RepoSubmitForm({
|
|
| 144 |
onChange={(e) => setRef(e.target.value)}
|
| 145 |
placeholder="Branch, tag, or commit SHA (optional)"
|
| 146 |
spellCheck={false}
|
| 147 |
-
className="h-8 w-72 rounded-
|
| 148 |
aria-label="Version to scan (branch, tag, or commit SHA)"
|
| 149 |
/>
|
| 150 |
) : (
|
| 151 |
<button
|
| 152 |
type="button"
|
| 153 |
onClick={() => setShowRef(true)}
|
| 154 |
-
className="text-[11px] text-
|
| 155 |
>
|
| 156 |
Scan a specific version
|
| 157 |
</button>
|
| 158 |
)}
|
| 159 |
</div>
|
| 160 |
|
| 161 |
-
<div className=
|
| 162 |
{error ? (
|
| 163 |
-
<span className="inline-flex items-start gap-1.5 text-
|
| 164 |
<CircleAlert size={14} className="mt-px shrink-0" />
|
| 165 |
<span>{error}</span>
|
| 166 |
</span>
|
| 167 |
) : pending ? (
|
| 168 |
-
<span className="text-
|
| 169 |
) : (
|
| 170 |
<span className="inline-flex items-center gap-1.5">
|
| 171 |
<CircleAlert size={13} strokeWidth={1.8} className="shrink-0" aria-hidden />
|
|
|
|
| 49 |
size = "default",
|
| 50 |
appearance = "default",
|
| 51 |
className = "",
|
| 52 |
+
align = "center",
|
| 53 |
}: {
|
| 54 |
size?: "default" | "hero";
|
| 55 |
appearance?: "default" | "dark";
|
| 56 |
className?: string;
|
| 57 |
+
align?: "center" | "left";
|
| 58 |
}) {
|
| 59 |
const nav = useNavigate();
|
| 60 |
const meQ = useMe();
|
|
|
|
| 95 |
};
|
| 96 |
|
| 97 |
if (dark) {
|
| 98 |
+
// hero composer(暗色控制台);appearance prop 名保留为调用方兼容
|
| 99 |
+
const centered = align === "center";
|
| 100 |
return (
|
| 101 |
<form
|
| 102 |
onSubmit={(e) => void onSubmit(e)}
|
|
|
|
| 105 |
aria-busy={pending}
|
| 106 |
>
|
| 107 |
<div
|
| 108 |
+
className={`openvuln-composer group flex min-h-[58px] items-center gap-3 rounded-xl border bg-surface-raised p-2 pl-4 shadow-[0_20px_48px_-20px_rgba(0,0,0,0.65)] transition focus-within:bg-surface-sunken/60 ${
|
| 109 |
+
error ? "border-danger/60" : "border-line focus-within:border-line-strong"
|
| 110 |
}`}
|
| 111 |
>
|
| 112 |
<Github
|
| 113 |
+
size={18}
|
| 114 |
+
className="shrink-0 text-ink-tertiary transition group-focus-within:text-ink-secondary"
|
| 115 |
/>
|
| 116 |
<input
|
| 117 |
type="url"
|
|
|
|
| 122 |
}}
|
| 123 |
placeholder="Paste a public GitHub repository URL"
|
| 124 |
spellCheck={false}
|
| 125 |
+
className="min-w-0 flex-1 bg-transparent py-3 font-mono text-[13px] text-ink outline-none placeholder:text-ink-tertiary sm:text-[14px]"
|
| 126 |
aria-label="GitHub repository URL"
|
| 127 |
aria-invalid={!!error}
|
| 128 |
/>
|
| 129 |
<button
|
| 130 |
type="submit"
|
| 131 |
aria-label={pending ? "Submitting repository" : "Analyze repository"}
|
| 132 |
+
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-ink text-surface shadow-sm transition hover:bg-white active:scale-95 focus-ring disabled:cursor-not-allowed disabled:bg-surface-sunken disabled:text-ink-tertiary disabled:opacity-100"
|
| 133 |
disabled={!url.trim() || pending}
|
| 134 |
>
|
| 135 |
{pending ? (
|
| 136 |
+
<LoaderCircle size={19} className="animate-spin motion-reduce:animate-none" />
|
| 137 |
) : (
|
| 138 |
+
<ArrowUp size={19} strokeWidth={2.2} />
|
| 139 |
)}
|
| 140 |
</button>
|
| 141 |
</div>
|
| 142 |
|
| 143 |
+
<div className={`mt-2.5 flex ${centered ? "justify-center" : "justify-start"}`}>
|
| 144 |
{showRef ? (
|
| 145 |
<input
|
| 146 |
type="text"
|
|
|
|
| 148 |
onChange={(e) => setRef(e.target.value)}
|
| 149 |
placeholder="Branch, tag, or commit SHA (optional)"
|
| 150 |
spellCheck={false}
|
| 151 |
+
className="h-8 w-72 rounded-lg border border-line bg-surface-raised px-3 font-mono text-xs text-ink outline-none placeholder:text-ink-tertiary focus:border-line-strong"
|
| 152 |
aria-label="Version to scan (branch, tag, or commit SHA)"
|
| 153 |
/>
|
| 154 |
) : (
|
| 155 |
<button
|
| 156 |
type="button"
|
| 157 |
onClick={() => setShowRef(true)}
|
| 158 |
+
className="font-mono text-[11px] text-ink-tertiary underline decoration-line underline-offset-2 transition hover:text-ink-secondary"
|
| 159 |
>
|
| 160 |
Scan a specific version
|
| 161 |
</button>
|
| 162 |
)}
|
| 163 |
</div>
|
| 164 |
|
| 165 |
+
<div className={`mt-2.5 flex min-h-5 items-start gap-1.5 text-xs text-ink-tertiary ${centered ? "justify-center text-center" : "justify-start text-left"}`}>
|
| 166 |
{error ? (
|
| 167 |
+
<span className="inline-flex items-start gap-1.5 text-danger" role="alert">
|
| 168 |
<CircleAlert size={14} className="mt-px shrink-0" />
|
| 169 |
<span>{error}</span>
|
| 170 |
</span>
|
| 171 |
) : pending ? (
|
| 172 |
+
<span className="text-ink-secondary" role="status">Submitting repository…</span>
|
| 173 |
) : (
|
| 174 |
<span className="inline-flex items-center gap-1.5">
|
| 175 |
<CircleAlert size={13} strokeWidth={1.8} className="shrink-0" aria-hidden />
|
src/components/ScanDurationNotice.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import { Clock3 } from "lucide-react";
|
|
| 4 |
export function ScanDurationNotice({ className = "" }: { className?: string }) {
|
| 5 |
return (
|
| 6 |
<p
|
| 7 |
-
className={`flex items-center justify-center gap-1.5 text-xs text-
|
| 8 |
role="note"
|
| 9 |
>
|
| 10 |
<Clock3 size={13} strokeWidth={1.8} className="shrink-0" />
|
|
|
|
| 4 |
export function ScanDurationNotice({ className = "" }: { className?: string }) {
|
| 5 |
return (
|
| 6 |
<p
|
| 7 |
+
className={`flex items-center justify-center gap-1.5 text-xs text-ink-tertiary ${className}`}
|
| 8 |
role="note"
|
| 9 |
>
|
| 10 |
<Clock3 size={13} strokeWidth={1.8} className="shrink-0" />
|
src/components/SeverityBar.tsx
CHANGED
|
@@ -1,17 +1,17 @@
|
|
| 1 |
import type { Severity, SeverityCounts } from "@/shared/types";
|
| 2 |
import { SEV_ORDER, severityLabel, totalFindings } from "../shared/lib/format";
|
| 3 |
|
| 4 |
-
/** NVD four tiers —
|
| 5 |
const BAR: Record<Severity, string> = {
|
| 6 |
-
critical: "#
|
| 7 |
-
high: "#
|
| 8 |
-
medium: "#
|
| 9 |
-
low: "#
|
| 10 |
};
|
| 11 |
|
| 12 |
const DOT: Record<Severity, string> = {
|
| 13 |
-
critical: "bg-
|
| 14 |
-
high: "bg-
|
| 15 |
medium: "bg-sev-medium-bar",
|
| 16 |
low: "bg-sev-low-bar",
|
| 17 |
};
|
|
|
|
| 1 |
import type { Severity, SeverityCounts } from "@/shared/types";
|
| 2 |
import { SEV_ORDER, severityLabel, totalFindings } from "../shared/lib/format";
|
| 3 |
|
| 4 |
+
/** NVD four tiers — dark-console spectrum, red → orange → amber. */
|
| 5 |
const BAR: Record<Severity, string> = {
|
| 6 |
+
critical: "#E5484D",
|
| 7 |
+
high: "#F2604F",
|
| 8 |
+
medium: "#FF8A3C",
|
| 9 |
+
low: "#F0C43A",
|
| 10 |
};
|
| 11 |
|
| 12 |
const DOT: Record<Severity, string> = {
|
| 13 |
+
critical: "bg-sev-critical-bar",
|
| 14 |
+
high: "bg-sev-high-bar",
|
| 15 |
medium: "bg-sev-medium-bar",
|
| 16 |
low: "bg-sev-low-bar",
|
| 17 |
};
|
src/features/home/ProductHomePage.tsx
CHANGED
|
@@ -1,37 +1,31 @@
|
|
| 1 |
import { useEffect, useMemo, useRef, useState } from "react";
|
| 2 |
import { Link } from "react-router-dom";
|
| 3 |
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
| 4 |
-
import {
|
| 5 |
-
|
| 6 |
-
ArrowRight,
|
| 7 |
-
CheckCircle2,
|
| 8 |
-
ChevronDown,
|
| 9 |
-
ChevronUp,
|
| 10 |
-
Clock3,
|
| 11 |
-
Github,
|
| 12 |
-
Radar,
|
| 13 |
-
Search,
|
| 14 |
-
Shield,
|
| 15 |
-
} from "lucide-react";
|
| 16 |
-
import type { LiveScanItem } from "@/shared/types";
|
| 17 |
import { api } from "../../shared/api/client";
|
| 18 |
import { Button } from "../../components/Button";
|
| 19 |
import { EmptyState } from "../../components/EmptyState";
|
| 20 |
-
import { ProjectRow } from "../../components/ProjectRow";
|
| 21 |
import { RepoSubmitForm } from "../../components/RepoSubmitForm";
|
| 22 |
-
import {
|
| 23 |
-
import { ScanDurationNotice } from "../../components/ScanDurationNotice";
|
| 24 |
import { AuthButton } from "../../components/AuthButton";
|
| 25 |
import { NotificationBell } from "../../components/NotificationBell";
|
|
|
|
| 26 |
|
| 27 |
const OWN_REPO =
|
| 28 |
(import.meta.env.VITE_GITHUB_REPO_URL as string | undefined) ||
|
| 29 |
"https://github.com/Clouditera/OpenVuln";
|
| 30 |
-
const ZAI_HF_AVATAR = "https://huggingface.co/api/avatars/zai-org";
|
| 31 |
|
| 32 |
/** List state survives project-detail navigation. */
|
| 33 |
const listCache = { sort: "stars" as "newest" | "stars", q: "" };
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
function formatElapsed(seconds: number): string {
|
| 36 |
if (seconds < 60) return `${seconds}s`;
|
| 37 |
const minutes = Math.floor(seconds / 60);
|
|
@@ -39,163 +33,365 @@ function formatElapsed(seconds: number): string {
|
|
| 39 |
return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
| 40 |
}
|
| 41 |
|
| 42 |
-
function BrandMark(
|
| 43 |
return (
|
| 44 |
-
<
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
aria-hidden
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
>
|
| 50 |
-
|
| 51 |
-
<img
|
| 52 |
-
src={ZAI_HF_AVATAR}
|
| 53 |
-
alt=""
|
| 54 |
-
className="absolute inset-0 h-full w-full object-cover"
|
| 55 |
-
onError={(event) => {
|
| 56 |
-
event.currentTarget.style.display = "none";
|
| 57 |
-
}}
|
| 58 |
-
/>
|
| 59 |
</div>
|
| 60 |
);
|
| 61 |
}
|
| 62 |
|
| 63 |
-
function
|
| 64 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
return (
|
| 67 |
-
<
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
</p>
|
| 79 |
-
<div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 font-mono text-[10px] text-ink-tertiary">
|
| 80 |
-
<span className="inline-flex items-center gap-1.5 text-[#b8bac2]">
|
| 81 |
-
<Activity size={10} />
|
| 82 |
-
{dispatching ? "Preparing scan" : "Scanning now"}
|
| 83 |
-
</span>
|
| 84 |
-
<span className="inline-flex items-center gap-1.5">
|
| 85 |
-
<Clock3 size={10} />
|
| 86 |
-
{formatElapsed(scan.elapsed_sec)}
|
| 87 |
</span>
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
</div>
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
</p>
|
| 94 |
-
|
| 95 |
-
|
|
|
|
| 96 |
</p>
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
);
|
| 104 |
}
|
| 105 |
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
loading,
|
| 110 |
failed,
|
| 111 |
}: {
|
| 112 |
-
|
| 113 |
-
queued: number;
|
| 114 |
loading: boolean;
|
| 115 |
failed: boolean;
|
| 116 |
}) {
|
| 117 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
return (
|
| 120 |
-
<
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
className="openvuln-panel relative min-w-0 overflow-hidden p-5 sm:p-6"
|
| 124 |
>
|
| 125 |
-
|
| 126 |
-
<
|
| 127 |
-
<
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
</span>
|
| 137 |
-
|
| 138 |
-
</
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
<
|
| 145 |
-
{active} active · {queued} queued
|
| 146 |
-
</div>
|
| 147 |
)}
|
| 148 |
-
</
|
| 149 |
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
<
|
| 159 |
-
<div>
|
| 160 |
-
<Activity size={20} className="mx-auto text-ink-tertiary" />
|
| 161 |
-
<p className="mt-2 text-sm font-medium text-ink-secondary">Live data is unavailable</p>
|
| 162 |
-
<p className="mt-1 text-xs text-ink-tertiary">Scanner status will reconnect automatically.</p>
|
| 163 |
-
</div>
|
| 164 |
-
</div>
|
| 165 |
-
) : scans.length > 0 ? (
|
| 166 |
-
<div className="space-y-2.5">
|
| 167 |
-
{scans.map((scan) => (
|
| 168 |
-
<ScanRow key={scan.project_id} scan={scan} />
|
| 169 |
-
))}
|
| 170 |
-
</div>
|
| 171 |
) : (
|
| 172 |
-
<
|
| 173 |
-
<div>
|
| 174 |
-
<CheckCircle2 size={21} className="mx-auto text-success-ink" />
|
| 175 |
-
<p className="mt-2 text-sm font-medium text-white">Scanner is ready</p>
|
| 176 |
-
<p className="mt-1 text-xs leading-5 text-ink-tertiary">
|
| 177 |
-
No repository is being scanned right now{queued > 0 ? ` · ${queued} waiting` : ""}.
|
| 178 |
-
</p>
|
| 179 |
-
</div>
|
| 180 |
-
</div>
|
| 181 |
)}
|
| 182 |
-
</
|
| 183 |
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
</section>
|
| 192 |
);
|
| 193 |
}
|
| 194 |
|
| 195 |
export function ProductHomePage() {
|
| 196 |
const [q, setQState] = useState(listCache.q);
|
| 197 |
const [sort, setSortState] = useState<"newest" | "stars">(listCache.sort);
|
| 198 |
-
const [repositoriesOpen, setRepositoriesOpen] = useState(false);
|
| 199 |
const setQ = (value: string) => {
|
| 200 |
listCache.q = value;
|
| 201 |
setQState(value);
|
|
@@ -221,23 +417,21 @@ export function ProductHomePage() {
|
|
| 221 |
});
|
| 222 |
const allItems = useMemo(() => projects.data?.pages.flatMap((page) => page.items) ?? [], [projects.data]);
|
| 223 |
const sentinelRef = useRef<HTMLDivElement>(null);
|
| 224 |
-
const repositoryScrollRef = useRef<HTMLDivElement>(null);
|
| 225 |
|
| 226 |
useEffect(() => {
|
| 227 |
const sentinel = sentinelRef.current;
|
| 228 |
-
|
| 229 |
-
if (!repositoriesOpen || !sentinel || !scrollContainer) return;
|
| 230 |
const observer = new IntersectionObserver(
|
| 231 |
(entries) => {
|
| 232 |
if (entries.some((entry) => entry.isIntersecting) && projects.hasNextPage && !projects.isFetchingNextPage) {
|
| 233 |
void projects.fetchNextPage();
|
| 234 |
}
|
| 235 |
},
|
| 236 |
-
{
|
| 237 |
);
|
| 238 |
observer.observe(sentinel);
|
| 239 |
return () => observer.disconnect();
|
| 240 |
-
}, [
|
| 241 |
|
| 242 |
const filtered = useMemo(() => {
|
| 243 |
const needle = q.trim().toLowerCase();
|
|
@@ -250,210 +444,181 @@ export function ProductHomePage() {
|
|
| 250 |
}, [allItems, q]);
|
| 251 |
|
| 252 |
const scans = overview.data?.live?.scanning ?? [];
|
| 253 |
-
const queued = overview.data?.live?.queued_count ?? 0;
|
| 254 |
|
| 255 |
return (
|
| 256 |
-
<div id="top" className="
|
| 257 |
-
<div className="openvuln-
|
| 258 |
-
|
| 259 |
-
<div
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
<
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
>
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
</div>
|
| 283 |
-
</header>
|
| 284 |
-
|
| 285 |
-
<main className="mx-auto w-full max-w-[1280px] px-4 pb-16 pt-8 sm:px-7 sm:pt-11 lg:px-9 lg:pb-20 lg:pt-14 xl:px-12">
|
| 286 |
-
<section id="submit" className="mx-auto flex max-w-[920px] scroll-mt-24 flex-col items-center text-center">
|
| 287 |
-
<div className="openvuln-brand flex flex-col items-center">
|
| 288 |
-
<div className="mb-4 flex items-center gap-2 font-mono text-[9px] font-semibold uppercase tracking-[0.2em] text-[#96989f]">
|
| 289 |
-
<span className="h-1.5 w-1.5 rounded-full bg-white/60" />
|
| 290 |
-
Open-source security scanner
|
| 291 |
-
</div>
|
| 292 |
-
<div className="flex items-center justify-center gap-4 sm:gap-5">
|
| 293 |
-
<div className="relative flex h-14 w-14 shrink-0 items-center justify-center overflow-hidden rounded-2xl border border-white/20 bg-white font-display text-2xl font-bold text-black shadow-[0_14px_44px_rgba(0,0,0,0.4)] sm:h-16 sm:w-16">
|
| 294 |
-
<span aria-hidden>Z</span>
|
| 295 |
-
<img
|
| 296 |
-
src={ZAI_HF_AVATAR}
|
| 297 |
-
alt="Z.ai"
|
| 298 |
-
className="absolute inset-0 h-full w-full object-cover"
|
| 299 |
-
onError={(event) => {
|
| 300 |
-
event.currentTarget.style.display = "none";
|
| 301 |
-
}}
|
| 302 |
-
/>
|
| 303 |
-
</div>
|
| 304 |
-
<h1 className="openvuln-title font-display text-[40px] font-[450] leading-none tracking-normal sm:text-[56px]">
|
| 305 |
-
OpenVuln
|
| 306 |
-
</h1>
|
| 307 |
-
</div>
|
| 308 |
-
<p className="mt-4 max-w-2xl text-sm leading-6 text-ink-secondary sm:text-[15px]">
|
| 309 |
-
GLM-powered vulnerability discovery for the open-source world.
|
| 310 |
-
</p>
|
| 311 |
-
</div>
|
| 312 |
|
| 313 |
-
|
| 314 |
-
<
|
| 315 |
-
|
| 316 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
</p>
|
| 318 |
-
<ScanDurationNotice className="mt-1.5 w-full" />
|
| 319 |
</div>
|
| 320 |
-
</section>
|
| 321 |
-
|
| 322 |
-
<div className="mt-9 grid items-stretch gap-4 xl:grid-cols-[0.88fr_1.12fr]">
|
| 323 |
-
<HeroStats stats={overview.data} loading={overview.isLoading} failed={overview.isError} />
|
| 324 |
-
<LiveScanPanel
|
| 325 |
-
scans={scans}
|
| 326 |
-
queued={queued}
|
| 327 |
-
loading={overview.isLoading}
|
| 328 |
-
failed={overview.isError}
|
| 329 |
-
/>
|
| 330 |
</div>
|
|
|
|
| 331 |
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
</div>
|
| 347 |
-
<
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
aria-expanded={repositoriesOpen}
|
| 351 |
-
aria-controls="repository-list-content"
|
| 352 |
-
className="inline-flex h-9 shrink-0 items-center gap-2 rounded-xl border border-white/[0.09] bg-black/20 px-3 text-[11px] font-medium text-[#b9bac0] transition hover:border-white/[0.16] hover:bg-white/[0.05] hover:text-white focus-ring-dark"
|
| 353 |
>
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
</div>
|
|
|
|
| 367 |
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
>
|
| 386 |
-
<button
|
| 387 |
-
type="button"
|
| 388 |
-
aria-pressed={sort === "stars"}
|
| 389 |
-
onClick={() => setSort("stars")}
|
| 390 |
-
className={`rounded-md px-3 py-1.5 text-[11px] font-medium transition focus-ring-dark ${
|
| 391 |
-
sort === "stars" ? "bg-white text-black" : "text-ink-tertiary hover:text-white"
|
| 392 |
-
}`}
|
| 393 |
-
>
|
| 394 |
-
Most starred
|
| 395 |
-
</button>
|
| 396 |
-
<button
|
| 397 |
-
type="button"
|
| 398 |
-
aria-pressed={sort === "newest"}
|
| 399 |
-
onClick={() => setSort("newest")}
|
| 400 |
-
className={`rounded-md px-3 py-1.5 text-[11px] font-medium transition focus-ring-dark ${
|
| 401 |
-
sort === "newest" ? "bg-white text-black" : "text-ink-tertiary hover:text-white"
|
| 402 |
-
}`}
|
| 403 |
-
>
|
| 404 |
-
Recently added
|
| 405 |
-
</button>
|
| 406 |
-
</div>
|
| 407 |
-
</div>
|
| 408 |
-
|
| 409 |
-
<div
|
| 410 |
-
ref={repositoryScrollRef}
|
| 411 |
-
className="openvuln-scrollbar mt-5 max-h-[520px] overflow-y-auto overscroll-contain border-y border-white/[0.07] pr-2"
|
| 412 |
-
tabIndex={0}
|
| 413 |
-
aria-label="Scanned repository list"
|
| 414 |
-
>
|
| 415 |
-
{projects.isLoading ? (
|
| 416 |
-
<div className="space-y-3 py-6">
|
| 417 |
-
{Array.from({ length: 5 }).map((_, index) => (
|
| 418 |
-
<div key={index} className="h-24 animate-pulse rounded-xl bg-white/[0.035]" />
|
| 419 |
-
))}
|
| 420 |
-
</div>
|
| 421 |
-
) : projects.isError ? (
|
| 422 |
-
<EmptyState icon={Shield} title="Could not load repositories" description="The OpenVuln API may be unavailable." />
|
| 423 |
-
) : filtered.length === 0 ? (
|
| 424 |
-
<EmptyState
|
| 425 |
-
icon={Shield}
|
| 426 |
-
title={q ? "No matching repositories" : "No repositories yet"}
|
| 427 |
-
description={
|
| 428 |
-
q
|
| 429 |
-
? "Try a different repository name."
|
| 430 |
-
: "Submit the first open-source repository to be scanned by VulnHunter."
|
| 431 |
-
}
|
| 432 |
-
action={!q ? <Link to="/submit"><Button>Submit a repository</Button></Link> : undefined}
|
| 433 |
-
/>
|
| 434 |
-
) : (
|
| 435 |
-
<>
|
| 436 |
-
{filtered.map((project) => (
|
| 437 |
-
<ProjectRow key={project.id} project={project} compact />
|
| 438 |
-
))}
|
| 439 |
-
<div ref={sentinelRef} className="h-px" />
|
| 440 |
-
{projects.isFetchingNextPage && (
|
| 441 |
-
<div className="py-5 text-center font-mono text-xs text-ink-tertiary">Loading more…</div>
|
| 442 |
-
)}
|
| 443 |
-
</>
|
| 444 |
-
)}
|
| 445 |
-
</div>
|
| 446 |
-
|
| 447 |
-
<div className="mt-4 flex items-center justify-between gap-3 font-mono text-[10px] text-ink-tertiary">
|
| 448 |
-
<span>{(projects.data?.pages[0]?.total ?? 0).toLocaleString()} repositories indexed</span>
|
| 449 |
-
<a href="#top" className="hover:text-white">Back to top ↑</a>
|
| 450 |
-
</div>
|
| 451 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
)}
|
| 453 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 454 |
|
| 455 |
-
</main>
|
| 456 |
-
</div>
|
| 457 |
</div>
|
| 458 |
);
|
| 459 |
}
|
|
|
|
| 1 |
import { useEffect, useMemo, useRef, useState } from "react";
|
| 2 |
import { Link } from "react-router-dom";
|
| 3 |
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
| 4 |
+
import { Clock3, Github, Search, Shield } from "lucide-react";
|
| 5 |
+
import type { LiveScanItem, OverviewStats, ProjectCard, Severity } from "@/shared/types";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
import { api } from "../../shared/api/client";
|
| 7 |
import { Button } from "../../components/Button";
|
| 8 |
import { EmptyState } from "../../components/EmptyState";
|
|
|
|
| 9 |
import { RepoSubmitForm } from "../../components/RepoSubmitForm";
|
| 10 |
+
import { SeverityBar } from "../../components/SeverityBar";
|
|
|
|
| 11 |
import { AuthButton } from "../../components/AuthButton";
|
| 12 |
import { NotificationBell } from "../../components/NotificationBell";
|
| 13 |
+
import { formatRelativeTime, formatStars, totalFindings } from "../../shared/lib/format";
|
| 14 |
|
| 15 |
const OWN_REPO =
|
| 16 |
(import.meta.env.VITE_GITHUB_REPO_URL as string | undefined) ||
|
| 17 |
"https://github.com/Clouditera/OpenVuln";
|
|
|
|
| 18 |
|
| 19 |
/** List state survives project-detail navigation. */
|
| 20 |
const listCache = { sort: "stars" as "newest" | "stars", q: "" };
|
| 21 |
|
| 22 |
+
const SEV_BARS: Array<{ level: Severity; color: string; label: string; short: string }> = [
|
| 23 |
+
{ level: "critical", color: "#E5484D", label: "Critical", short: "Crit" },
|
| 24 |
+
{ level: "high", color: "#F2604F", label: "High", short: "High" },
|
| 25 |
+
{ level: "medium", color: "#FF8A3C", label: "Medium", short: "Med" },
|
| 26 |
+
{ level: "low", color: "#F0C43A", label: "Low", short: "Low" },
|
| 27 |
+
];
|
| 28 |
+
|
| 29 |
function formatElapsed(seconds: number): string {
|
| 30 |
if (seconds < 60) return `${seconds}s`;
|
| 31 |
const minutes = Math.floor(seconds / 60);
|
|
|
|
| 33 |
return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
| 34 |
}
|
| 35 |
|
| 36 |
+
function BrandMark() {
|
| 37 |
return (
|
| 38 |
+
<img
|
| 39 |
+
src="/logo.svg"
|
| 40 |
+
alt=""
|
| 41 |
+
className="h-8 w-8 shrink-0 rounded-lg"
|
| 42 |
aria-hidden
|
| 43 |
+
/>
|
| 44 |
+
);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
/* ── 滚动进入视口时的克制上浮(一次性) ── */
|
| 48 |
+
function Reveal({
|
| 49 |
+
children,
|
| 50 |
+
delay = 0,
|
| 51 |
+
className = "",
|
| 52 |
+
}: {
|
| 53 |
+
children: React.ReactNode;
|
| 54 |
+
delay?: number;
|
| 55 |
+
className?: string;
|
| 56 |
+
}) {
|
| 57 |
+
const ref = useRef<HTMLDivElement>(null);
|
| 58 |
+
const [inView, setInView] = useState(false);
|
| 59 |
+
useEffect(() => {
|
| 60 |
+
const el = ref.current;
|
| 61 |
+
if (!el) return;
|
| 62 |
+
const observer = new IntersectionObserver(
|
| 63 |
+
(entries) => {
|
| 64 |
+
if (entries.some((entry) => entry.isIntersecting)) {
|
| 65 |
+
setInView(true);
|
| 66 |
+
observer.disconnect();
|
| 67 |
+
}
|
| 68 |
+
},
|
| 69 |
+
{ rootMargin: "0px 0px -6% 0px" },
|
| 70 |
+
);
|
| 71 |
+
observer.observe(el);
|
| 72 |
+
return () => observer.disconnect();
|
| 73 |
+
}, []);
|
| 74 |
+
return (
|
| 75 |
+
<div
|
| 76 |
+
ref={ref}
|
| 77 |
+
style={delay ? { transitionDelay: `${delay}ms` } : undefined}
|
| 78 |
+
className={`ov-reveal ${inView ? "ov-in" : ""} ${className}`}
|
| 79 |
>
|
| 80 |
+
{children}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
</div>
|
| 82 |
);
|
| 83 |
}
|
| 84 |
|
| 85 |
+
function usePrefersReducedMotion(): boolean {
|
| 86 |
+
const [reduced, setReduced] = useState(false);
|
| 87 |
+
useEffect(() => {
|
| 88 |
+
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
|
| 89 |
+
setReduced(media.matches);
|
| 90 |
+
const update = () => setReduced(media.matches);
|
| 91 |
+
media.addEventListener("change", update);
|
| 92 |
+
return () => media.removeEventListener("change", update);
|
| 93 |
+
}, []);
|
| 94 |
+
return reduced;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
function useCountUp(target: number, duration = 700): number {
|
| 98 |
+
const reduced = usePrefersReducedMotion();
|
| 99 |
+
const [value, setValue] = useState(reduced ? target : 0);
|
| 100 |
+
useEffect(() => {
|
| 101 |
+
if (reduced) {
|
| 102 |
+
setValue(target);
|
| 103 |
+
return;
|
| 104 |
+
}
|
| 105 |
+
let frame = 0;
|
| 106 |
+
const start = performance.now();
|
| 107 |
+
const tick = (time: number) => {
|
| 108 |
+
const progress = Math.min(1, (time - start) / duration);
|
| 109 |
+
setValue(Math.round(target * (1 - (1 - progress) ** 3)));
|
| 110 |
+
if (progress < 1) frame = requestAnimationFrame(tick);
|
| 111 |
+
};
|
| 112 |
+
frame = requestAnimationFrame(tick);
|
| 113 |
+
return () => cancelAnimationFrame(frame);
|
| 114 |
+
}, [duration, reduced, target]);
|
| 115 |
+
return value;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
function AnimatedNumber({ value }: { value: number }) {
|
| 119 |
+
return <>{useCountUp(value).toLocaleString()}</>;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
/* ── 控制台带子共用网格:三条带子的竖向 hairline 严格同轴 ── */
|
| 123 |
+
const BAND_GRID = "grid divide-y divide-line lg:grid-cols-4 lg:divide-y-0 lg:divide-x";
|
| 124 |
+
const BAND_CELL = "py-5 lg:px-6 lg:first:pl-0";
|
| 125 |
+
|
| 126 |
+
/* ── 实时信息板(departures strip):正在扫描的仓库,全宽横条 ── */
|
| 127 |
+
function LiveStrip({
|
| 128 |
+
scans,
|
| 129 |
+
loading,
|
| 130 |
+
failed,
|
| 131 |
+
}: {
|
| 132 |
+
scans: LiveScanItem[];
|
| 133 |
+
loading: boolean;
|
| 134 |
+
failed: boolean;
|
| 135 |
+
}) {
|
| 136 |
+
const active = scans.length;
|
| 137 |
+
// 单行 4 列:1 列标签 + 3 列条目;超过 3 个时末列汇总
|
| 138 |
+
const shown = active > 3 ? scans.slice(0, 2) : scans;
|
| 139 |
+
const overflow = active - shown.length;
|
| 140 |
+
const fillers = Math.max(0, 3 - shown.length - (overflow > 0 ? 1 : 0));
|
| 141 |
|
| 142 |
return (
|
| 143 |
+
<section aria-labelledby="ops-title" className={`border-t border-line ${BAND_GRID}`}>
|
| 144 |
+
<div className={`flex items-center justify-between gap-3 lg:flex-col lg:items-start lg:justify-center ${BAND_CELL}`}>
|
| 145 |
+
<h2
|
| 146 |
+
id="ops-title"
|
| 147 |
+
className="flex items-center gap-2.5 font-mono text-[10px] font-medium uppercase tracking-[0.22em] text-ink-secondary"
|
| 148 |
+
>
|
| 149 |
+
<span className="relative flex h-1.5 w-1.5">
|
| 150 |
+
{active > 0 && (
|
| 151 |
+
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-success opacity-60 motion-reduce:hidden" />
|
| 152 |
+
)}
|
| 153 |
+
<span className={`relative inline-flex h-1.5 w-1.5 rounded-full ${active > 0 ? "bg-success" : "bg-ink-tertiary"}`} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
</span>
|
| 155 |
+
Live — scanning now
|
| 156 |
+
</h2>
|
| 157 |
+
{!loading && !failed && (
|
| 158 |
+
<p className="font-mono text-[10px] tabular-nums text-ink-tertiary lg:mt-2">
|
| 159 |
+
{active} active
|
| 160 |
+
</p>
|
| 161 |
+
)}
|
| 162 |
</div>
|
| 163 |
+
|
| 164 |
+
{loading ? (
|
| 165 |
+
[0, 1, 2].map((item) => (
|
| 166 |
+
<div key={item} className={`flex items-center ${BAND_CELL}`} aria-hidden>
|
| 167 |
+
<div className="h-9 w-full animate-pulse rounded-md bg-surface-sunken/70" />
|
| 168 |
+
</div>
|
| 169 |
+
))
|
| 170 |
+
) : failed ? (
|
| 171 |
+
<p className={`flex items-center font-mono text-[11px] uppercase tracking-[0.14em] text-ink-tertiary lg:col-span-3 ${BAND_CELL}`}>
|
| 172 |
+
Signal lost — live data reconnects automatically
|
| 173 |
</p>
|
| 174 |
+
) : active === 0 ? (
|
| 175 |
+
<p className={`flex items-center font-mono text-[11px] uppercase tracking-[0.14em] text-ink-tertiary lg:col-span-3 ${BAND_CELL}`}>
|
| 176 |
+
Standing by — no repository is being scanned right now
|
| 177 |
</p>
|
| 178 |
+
) : (
|
| 179 |
+
<>
|
| 180 |
+
{shown.map((scan) => (
|
| 181 |
+
<Link
|
| 182 |
+
key={scan.project_id}
|
| 183 |
+
to={`/p/${scan.full_name}`}
|
| 184 |
+
className={`group flex min-w-0 items-center justify-between gap-4 transition-colors hover:bg-surface-sunken/60 focus-ring ${BAND_CELL}`}
|
| 185 |
+
>
|
| 186 |
+
<span className="min-w-0">
|
| 187 |
+
<span className="block truncate font-mono text-[13px] font-medium text-ink group-hover:text-white">
|
| 188 |
+
{scan.full_name}
|
| 189 |
+
</span>
|
| 190 |
+
<span className="mt-1 block font-mono text-[10px] uppercase tracking-[0.1em] text-ink-tertiary">
|
| 191 |
+
{scan.state === "dispatching" ? "preparing" : "scanning"} · {formatElapsed(scan.elapsed_sec)}
|
| 192 |
+
</span>
|
| 193 |
+
</span>
|
| 194 |
+
<span className="shrink-0 text-right">
|
| 195 |
+
<span className="block font-mono text-[16px] font-semibold tabular-nums text-ink">
|
| 196 |
+
{scan.findings_so_far}
|
| 197 |
+
</span>
|
| 198 |
+
<span className="block font-mono text-[9px] uppercase tracking-[0.12em] text-ink-tertiary">found</span>
|
| 199 |
+
</span>
|
| 200 |
+
</Link>
|
| 201 |
+
))}
|
| 202 |
+
{overflow > 0 && (
|
| 203 |
+
<p className={`flex items-center font-mono text-[11px] uppercase tracking-[0.14em] text-ink-tertiary ${BAND_CELL}`}>
|
| 204 |
+
+{overflow} more scanning
|
| 205 |
+
</p>
|
| 206 |
+
)}
|
| 207 |
+
{Array.from({ length: fillers }).map((_, index) => (
|
| 208 |
+
<div key={`filler-${index}`} className="hidden lg:block" aria-hidden />
|
| 209 |
+
))}
|
| 210 |
+
</>
|
| 211 |
+
)}
|
| 212 |
+
</section>
|
| 213 |
);
|
| 214 |
}
|
| 215 |
|
| 216 |
+
/* ── 指标带:账本数字 + severity 光谱,与顶部饰带互为图例 ── */
|
| 217 |
+
function MetricsBand({
|
| 218 |
+
stats,
|
| 219 |
loading,
|
| 220 |
failed,
|
| 221 |
}: {
|
| 222 |
+
stats: OverviewStats | undefined;
|
|
|
|
| 223 |
loading: boolean;
|
| 224 |
failed: boolean;
|
| 225 |
}) {
|
| 226 |
+
const unavailable = loading || failed || !stats;
|
| 227 |
+
const scanned = stats
|
| 228 |
+
? stats.scanned_project_count ?? Math.min(stats.project_count, stats.scan_completed_count)
|
| 229 |
+
: 0;
|
| 230 |
+
const counts = stats?.severity_counts ?? { critical: 0, high: 0, medium: 0, low: 0 };
|
| 231 |
+
|
| 232 |
+
const cells: Array<{ label: string; value: number }> = [
|
| 233 |
+
{ label: "Repositories scanned", value: scanned },
|
| 234 |
+
{ label: "Findings discovered", value: stats?.finding_total ?? 0 },
|
| 235 |
+
{ label: "Scans in progress", value: stats?.scan_in_progress_count ?? stats?.live?.scanning?.length ?? 0 },
|
| 236 |
+
];
|
| 237 |
+
|
| 238 |
+
return (
|
| 239 |
+
<section aria-label="Platform metrics" className={`border-y border-line ${BAND_GRID}`}>
|
| 240 |
+
{cells.map((cell) => (
|
| 241 |
+
<div key={cell.label} className={BAND_CELL}>
|
| 242 |
+
<p className="font-mono text-3xl font-medium tabular-nums tracking-tight text-ink sm:text-[36px]">
|
| 243 |
+
{unavailable ? "—" : <AnimatedNumber value={cell.value} />}
|
| 244 |
+
</p>
|
| 245 |
+
<p className="mt-2 font-mono text-[10px] uppercase tracking-[0.16em] text-ink-tertiary">{cell.label}</p>
|
| 246 |
+
</div>
|
| 247 |
+
))}
|
| 248 |
+
<div className={`flex flex-col justify-between gap-4 ${BAND_CELL}`}>
|
| 249 |
+
<div className="space-y-2" role="img" aria-label="Findings by severity">
|
| 250 |
+
{SEV_BARS.map((s) => {
|
| 251 |
+
const count = counts[s.level] ?? 0;
|
| 252 |
+
const max = Math.max(1, ...SEV_BARS.map((x) => counts[x.level] ?? 0));
|
| 253 |
+
return (
|
| 254 |
+
<div key={s.level} className="flex items-center gap-2.5">
|
| 255 |
+
<span className="w-8 shrink-0 font-mono text-[9px] uppercase tracking-[0.08em] text-ink-tertiary">
|
| 256 |
+
{s.short}
|
| 257 |
+
</span>
|
| 258 |
+
<span className="h-[3px] flex-1 overflow-hidden rounded-full bg-surface-sunken">
|
| 259 |
+
<span
|
| 260 |
+
className="block h-full rounded-full transition-[width] duration-700 ease-out"
|
| 261 |
+
style={{
|
| 262 |
+
width: unavailable ? "0%" : `${(count / max) * 100}%`,
|
| 263 |
+
backgroundColor: s.color,
|
| 264 |
+
}}
|
| 265 |
+
/>
|
| 266 |
+
</span>
|
| 267 |
+
<span className="w-7 shrink-0 text-right font-mono text-[12px] tabular-nums text-ink">
|
| 268 |
+
{unavailable ? "—" : count.toLocaleString()}
|
| 269 |
+
</span>
|
| 270 |
+
</div>
|
| 271 |
+
);
|
| 272 |
+
})}
|
| 273 |
+
</div>
|
| 274 |
+
<p className="font-mono text-[10px] uppercase tracking-[0.16em] text-ink-tertiary">
|
| 275 |
+
Findings by severity
|
| 276 |
+
</p>
|
| 277 |
+
</div>
|
| 278 |
+
</section>
|
| 279 |
+
);
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
/* ── 仓库账本行 ── */
|
| 283 |
+
function LedgerAvatar({ owner }: { owner: string }) {
|
| 284 |
+
const [failed, setFailed] = useState(false);
|
| 285 |
+
if (failed) {
|
| 286 |
+
return (
|
| 287 |
+
<span
|
| 288 |
+
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-surface-sunken font-mono text-[10px] font-semibold text-ink-secondary"
|
| 289 |
+
aria-hidden
|
| 290 |
+
>
|
| 291 |
+
{owner.slice(0, 1).toUpperCase()}
|
| 292 |
+
</span>
|
| 293 |
+
);
|
| 294 |
+
}
|
| 295 |
+
return (
|
| 296 |
+
<img
|
| 297 |
+
src={`https://github.com/${owner}.png?size=64`}
|
| 298 |
+
alt=""
|
| 299 |
+
width={28}
|
| 300 |
+
height={28}
|
| 301 |
+
className="h-7 w-7 shrink-0 rounded-full border border-line bg-surface-sunken"
|
| 302 |
+
onError={() => setFailed(true)}
|
| 303 |
+
/>
|
| 304 |
+
);
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
function LedgerRow({ project }: { project: ProjectCard }) {
|
| 308 |
+
const state = project.latest_scan?.state;
|
| 309 |
+
const scanning = state === "scanning" || state === "dispatching";
|
| 310 |
+
const waiting = state !== "completed" && !scanning;
|
| 311 |
+
const soFar = project.latest_scan?.findings_so_far ?? 0;
|
| 312 |
+
const findings = totalFindings(project.severity_counts);
|
| 313 |
|
| 314 |
return (
|
| 315 |
+
<Link
|
| 316 |
+
to={`/p/${project.owner_login}/${project.name}`}
|
| 317 |
+
className="group grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-4 gap-y-2 border-b border-line/70 px-2 py-3.5 transition-colors last:border-b-0 hover:bg-surface-sunken/60 focus-ring md:grid-cols-[minmax(0,2.4fr)_110px_78px_150px_70px_150px] md:gap-x-5"
|
|
|
|
| 318 |
>
|
| 319 |
+
{/* repository */}
|
| 320 |
+
<span className="flex min-w-0 items-center gap-3">
|
| 321 |
+
<LedgerAvatar owner={project.owner_login} />
|
| 322 |
+
<span className="min-w-0">
|
| 323 |
+
<span className="block truncate font-mono text-[13px] font-medium text-ink group-hover:text-white">
|
| 324 |
+
{project.full_name}
|
| 325 |
+
</span>
|
| 326 |
+
{project.description && (
|
| 327 |
+
<span className="mt-0.5 block truncate text-[12px] text-ink-tertiary">{project.description}</span>
|
| 328 |
+
)}
|
| 329 |
+
</span>
|
| 330 |
+
</span>
|
| 331 |
+
|
| 332 |
+
{/* language */}
|
| 333 |
+
<span className="hidden min-w-0 items-center gap-1.5 truncate text-[12px] text-ink-secondary md:flex">
|
| 334 |
+
{project.language ? (
|
| 335 |
+
<>
|
| 336 |
+
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-ink/40" />
|
| 337 |
+
<span className="truncate">{project.language}</span>
|
| 338 |
+
</>
|
| 339 |
+
) : (
|
| 340 |
+
<span className="text-ink-tertiary">—</span>
|
| 341 |
+
)}
|
| 342 |
+
</span>
|
| 343 |
+
|
| 344 |
+
{/* stars */}
|
| 345 |
+
<span className="hidden font-mono text-[12px] tabular-nums text-ink-secondary md:block">
|
| 346 |
+
★ {formatStars(project.stars)}
|
| 347 |
+
</span>
|
| 348 |
+
|
| 349 |
+
{/* status */}
|
| 350 |
+
<span className="hidden md:block">
|
| 351 |
+
{scanning ? (
|
| 352 |
+
<span className="inline-flex items-center gap-1.5 font-mono text-[11px] uppercase tracking-[0.06em] text-ink">
|
| 353 |
+
<span className="relative flex h-1.5 w-1.5">
|
| 354 |
+
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-ink opacity-40 motion-reduce:animate-none" />
|
| 355 |
+
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-ink" />
|
| 356 |
</span>
|
| 357 |
+
scanning{soFar > 0 ? ` · ${soFar}` : ""}
|
| 358 |
+
</span>
|
| 359 |
+
) : waiting ? (
|
| 360 |
+
<span className="font-mono text-[11px] uppercase tracking-[0.06em] text-ink-tertiary">pending</span>
|
| 361 |
+
) : (
|
| 362 |
+
<span className="font-mono text-[11px] text-ink-secondary">
|
| 363 |
+
{project.latest_scan?.finished_at ? formatRelativeTime(project.latest_scan.finished_at) : "done"}
|
| 364 |
+
</span>
|
|
|
|
|
|
|
| 365 |
)}
|
| 366 |
+
</span>
|
| 367 |
|
| 368 |
+
{/* findings count */}
|
| 369 |
+
<span className="text-right font-mono text-[13px] font-medium tabular-nums text-ink md:text-left">
|
| 370 |
+
{waiting || (scanning && findings === 0) ? <span className="text-ink-tertiary">—</span> : findings}
|
| 371 |
+
</span>
|
| 372 |
+
|
| 373 |
+
{/* severity */}
|
| 374 |
+
<span className="col-span-2 md:col-span-1">
|
| 375 |
+
{!waiting && !scanning && findings > 0 ? (
|
| 376 |
+
<SeverityBar counts={project.severity_counts} showLegend={false} widthClass="w-full" heightClass="h-1" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
) : (
|
| 378 |
+
<span className="block h-1 w-full rounded-full bg-surface-sunken md:w-full" aria-hidden />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
)}
|
| 380 |
+
</span>
|
| 381 |
|
| 382 |
+
{/* mobile meta line */}
|
| 383 |
+
<span className="col-span-2 flex flex-wrap items-center gap-x-3 font-mono text-[10px] uppercase tracking-[0.06em] text-ink-tertiary md:hidden">
|
| 384 |
+
{project.language && <span>{project.language}</span>}
|
| 385 |
+
<span>★ {formatStars(project.stars)}</span>
|
| 386 |
+
{scanning ? <span className="text-ink">scanning{soFar > 0 ? ` · ${soFar}` : ""}</span> : waiting ? <span>pending</span> : project.latest_scan?.finished_at ? <span>{formatRelativeTime(project.latest_scan.finished_at)}</span> : null}
|
| 387 |
+
</span>
|
| 388 |
+
</Link>
|
|
|
|
| 389 |
);
|
| 390 |
}
|
| 391 |
|
| 392 |
export function ProductHomePage() {
|
| 393 |
const [q, setQState] = useState(listCache.q);
|
| 394 |
const [sort, setSortState] = useState<"newest" | "stars">(listCache.sort);
|
|
|
|
| 395 |
const setQ = (value: string) => {
|
| 396 |
listCache.q = value;
|
| 397 |
setQState(value);
|
|
|
|
| 417 |
});
|
| 418 |
const allItems = useMemo(() => projects.data?.pages.flatMap((page) => page.items) ?? [], [projects.data]);
|
| 419 |
const sentinelRef = useRef<HTMLDivElement>(null);
|
|
|
|
| 420 |
|
| 421 |
useEffect(() => {
|
| 422 |
const sentinel = sentinelRef.current;
|
| 423 |
+
if (!sentinel) return;
|
|
|
|
| 424 |
const observer = new IntersectionObserver(
|
| 425 |
(entries) => {
|
| 426 |
if (entries.some((entry) => entry.isIntersecting) && projects.hasNextPage && !projects.isFetchingNextPage) {
|
| 427 |
void projects.fetchNextPage();
|
| 428 |
}
|
| 429 |
},
|
| 430 |
+
{ rootMargin: "240px" },
|
| 431 |
);
|
| 432 |
observer.observe(sentinel);
|
| 433 |
return () => observer.disconnect();
|
| 434 |
+
}, [projects.hasNextPage, projects.isFetchingNextPage, projects.fetchNextPage]);
|
| 435 |
|
| 436 |
const filtered = useMemo(() => {
|
| 437 |
const needle = q.trim().toLowerCase();
|
|
|
|
| 444 |
}, [allItems, q]);
|
| 445 |
|
| 446 |
const scans = overview.data?.live?.scanning ?? [];
|
|
|
|
| 447 |
|
| 448 |
return (
|
| 449 |
+
<div id="top" className="relative isolate min-h-screen overflow-hidden bg-surface text-ink">
|
| 450 |
+
<div className="openvuln-hero-video" aria-hidden>
|
| 451 |
+
<video src="/hero-bg.mp4" poster="/hero-bg-poster.jpg" autoPlay muted loop playsInline preload="auto" />
|
| 452 |
+
</div>
|
| 453 |
+
|
| 454 |
+
<header className="sticky top-0 z-30 border-b border-line bg-surface/85 backdrop-blur-xl">
|
| 455 |
+
<div className="mx-auto flex h-16 w-full max-w-[1200px] items-center justify-between px-5 sm:px-8">
|
| 456 |
+
<Link to="/" className="flex items-center gap-2.5 rounded-lg focus-ring">
|
| 457 |
+
<BrandMark />
|
| 458 |
+
<span className="font-display text-[15px] font-semibold tracking-tight text-ink">OpenVuln</span>
|
| 459 |
+
</Link>
|
| 460 |
+
<div className="flex items-center gap-2">
|
| 461 |
+
<NotificationBell />
|
| 462 |
+
<AuthButton />
|
| 463 |
+
<a
|
| 464 |
+
href={OWN_REPO}
|
| 465 |
+
target="_blank"
|
| 466 |
+
rel="noreferrer"
|
| 467 |
+
aria-label="OpenVuln on GitHub"
|
| 468 |
+
className="hidden h-9 items-center gap-2 rounded-full border border-line bg-surface-raised px-3.5 text-xs font-medium text-ink-secondary transition hover:border-line-strong hover:text-ink focus-ring sm:inline-flex"
|
| 469 |
+
>
|
| 470 |
+
<Github size={14} />
|
| 471 |
+
GitHub
|
| 472 |
+
</a>
|
| 473 |
+
</div>
|
| 474 |
+
</div>
|
| 475 |
+
</header>
|
| 476 |
+
|
| 477 |
+
<main className="mx-auto w-full max-w-[1200px] px-5 pb-16 sm:px-8">
|
| 478 |
+
{/* ── Hero:大字报 —— 整幅展示字 + 出版物边注 ── */}
|
| 479 |
+
<section id="submit" className="scroll-mt-24 pt-10 sm:pt-14">
|
| 480 |
+
<div className="openvuln-brand">
|
| 481 |
+
<h1 className="mt-10 font-display font-medium uppercase leading-[0.94] tracking-[-0.015em] sm:mt-12">
|
| 482 |
+
<span className="ov-line-mask">
|
| 483 |
+
<span className="ov-line openvuln-outline block text-[15vw] sm:text-[clamp(64px,9.6vw,116px)]" style={{ animationDelay: "80ms" }}>
|
| 484 |
+
Open source<span className="openvuln-title-comma">,</span>
|
| 485 |
+
</span>
|
| 486 |
+
</span>
|
| 487 |
+
<span className="ov-line-mask">
|
| 488 |
+
<span className="ov-line openvuln-solid block text-[15vw] sm:text-[clamp(64px,9.6vw,116px)]" style={{ animationDelay: "200ms" }}>
|
| 489 |
+
audited by GLM<span className="sr-only">.</span><span className="openvuln-title-mark openvuln-title-period" aria-hidden />
|
| 490 |
+
</span>
|
| 491 |
+
</span>
|
| 492 |
+
</h1>
|
| 493 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
|
| 495 |
+
<div className="openvuln-composer mt-10 grid items-end gap-8 pb-12 sm:mt-14 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)] lg:gap-16 lg:pb-16" style={{ animationDelay: "380ms" }}>
|
| 496 |
+
<p className="max-w-[44ch] text-[15px] leading-7 text-ink-secondary">
|
| 497 |
+
Submit a public repository and OpenVuln hunts its vulnerabilities.
|
| 498 |
+
Maintainers see every finding — the world sees only what they disclose.
|
| 499 |
+
</p>
|
| 500 |
+
<div className="w-full lg:max-w-[600px] lg:justify-self-end">
|
| 501 |
+
<RepoSubmitForm size="hero" appearance="dark" className="w-full" align="left" />
|
| 502 |
+
<p className="mt-2 flex items-center gap-1.5 text-xs leading-5 text-ink-tertiary" role="note">
|
| 503 |
+
<Clock3 size={12} strokeWidth={1.8} aria-hidden />
|
| 504 |
+
Submissions may require manual review · A full scan usually takes 12+ hours
|
| 505 |
</p>
|
|
|
|
| 506 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
</div>
|
| 508 |
+
</section>
|
| 509 |
|
| 510 |
+
{/* ── 实时信息板 ── */}
|
| 511 |
+
<Reveal>
|
| 512 |
+
<LiveStrip scans={scans} loading={overview.isLoading} failed={overview.isError} />
|
| 513 |
+
</Reveal>
|
| 514 |
+
|
| 515 |
+
{/* ── 指标带 ── */}
|
| 516 |
+
<Reveal delay={60}>
|
| 517 |
+
<MetricsBand stats={overview.data} loading={overview.isLoading} failed={overview.isError} />
|
| 518 |
+
</Reveal>
|
| 519 |
+
|
| 520 |
+
{/* ── 仓库账本 ── */}
|
| 521 |
+
<section id="repositories" aria-labelledby="projects-title" className="scroll-mt-24 pt-12">
|
| 522 |
+
<Reveal className="flex flex-wrap items-end justify-between gap-4">
|
| 523 |
+
<div>
|
| 524 |
+
<p className="font-mono text-[10px] font-medium uppercase tracking-[0.22em] text-ink-tertiary">
|
| 525 |
+
Repository index
|
| 526 |
+
</p>
|
| 527 |
+
<h2 id="projects-title" className="mt-2 font-display text-2xl font-semibold tracking-tight text-ink">
|
| 528 |
+
Scanned repositories
|
| 529 |
+
</h2>
|
| 530 |
+
</div>
|
| 531 |
+
<div className="flex flex-col gap-2.5 sm:flex-row sm:items-center">
|
| 532 |
+
<div className="relative">
|
| 533 |
+
<Search size={14} className="pointer-events-none absolute left-3.5 top-1/2 -translate-y-1/2 text-ink-tertiary" />
|
| 534 |
+
<input
|
| 535 |
+
type="search"
|
| 536 |
+
value={q}
|
| 537 |
+
onChange={(event) => setQ(event.target.value)}
|
| 538 |
+
placeholder="Search repositories"
|
| 539 |
+
aria-label="Search repositories"
|
| 540 |
+
className="h-9 w-full rounded-lg border border-line bg-surface-raised pl-9 pr-3 font-mono text-[13px] text-ink placeholder:text-ink-tertiary focus-ring sm:w-64"
|
| 541 |
+
/>
|
| 542 |
</div>
|
| 543 |
+
<div
|
| 544 |
+
className="inline-flex self-start rounded-lg border border-line bg-surface-raised p-0.5 sm:self-auto"
|
| 545 |
+
aria-label="Sort repositories"
|
|
|
|
|
|
|
|
|
|
| 546 |
>
|
| 547 |
+
<button
|
| 548 |
+
type="button"
|
| 549 |
+
aria-pressed={sort === "stars"}
|
| 550 |
+
onClick={() => setSort("stars")}
|
| 551 |
+
className={`rounded-md px-3 py-1.5 font-mono text-[11px] font-medium transition focus-ring ${
|
| 552 |
+
sort === "stars" ? "bg-ink text-surface" : "text-ink-tertiary hover:text-ink"
|
| 553 |
+
}`}
|
| 554 |
+
>
|
| 555 |
+
Most starred
|
| 556 |
+
</button>
|
| 557 |
+
<button
|
| 558 |
+
type="button"
|
| 559 |
+
aria-pressed={sort === "newest"}
|
| 560 |
+
onClick={() => setSort("newest")}
|
| 561 |
+
className={`rounded-md px-3 py-1.5 font-mono text-[11px] font-medium transition focus-ring ${
|
| 562 |
+
sort === "newest" ? "bg-ink text-surface" : "text-ink-tertiary hover:text-ink"
|
| 563 |
+
}`}
|
| 564 |
+
>
|
| 565 |
+
Recently added
|
| 566 |
+
</button>
|
| 567 |
+
</div>
|
| 568 |
</div>
|
| 569 |
+
</Reveal>
|
| 570 |
|
| 571 |
+
{/* 表头 */}
|
| 572 |
+
<div className="mt-6 hidden grid-cols-[minmax(0,2.4fr)_110px_78px_150px_70px_150px] gap-x-5 border-b border-line px-2 pb-2.5 font-mono text-[10px] font-medium uppercase tracking-[0.16em] text-ink-tertiary md:grid">
|
| 573 |
+
<span>Repository</span>
|
| 574 |
+
<span>Language</span>
|
| 575 |
+
<span>Stars</span>
|
| 576 |
+
<span>Last scan</span>
|
| 577 |
+
<span>Findings</span>
|
| 578 |
+
<span>Severity</span>
|
| 579 |
+
</div>
|
| 580 |
+
<div className="mt-6 border-b border-line md:hidden" aria-hidden />
|
| 581 |
+
|
| 582 |
+
<div aria-label="Scanned repository list">
|
| 583 |
+
{projects.isLoading ? (
|
| 584 |
+
<div className="space-y-2 py-5">
|
| 585 |
+
{Array.from({ length: 6 }).map((_, index) => (
|
| 586 |
+
<div key={index} className="h-14 animate-pulse rounded-lg bg-surface-sunken/70" />
|
| 587 |
+
))}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 588 |
</div>
|
| 589 |
+
) : projects.isError ? (
|
| 590 |
+
<EmptyState icon={Shield} title="Could not load repositories" description="The OpenVuln API may be unavailable." />
|
| 591 |
+
) : filtered.length === 0 ? (
|
| 592 |
+
<EmptyState
|
| 593 |
+
icon={Shield}
|
| 594 |
+
title={q ? "No matching repositories" : "No repositories yet"}
|
| 595 |
+
description={
|
| 596 |
+
q
|
| 597 |
+
? "Try a different repository name."
|
| 598 |
+
: "Submit the first open-source repository to be scanned by VulnHunter."
|
| 599 |
+
}
|
| 600 |
+
action={!q ? <Link to="/submit"><Button>Submit a repository</Button></Link> : undefined}
|
| 601 |
+
/>
|
| 602 |
+
) : (
|
| 603 |
+
<>
|
| 604 |
+
{filtered.map((project) => (
|
| 605 |
+
<LedgerRow key={project.id} project={project} />
|
| 606 |
+
))}
|
| 607 |
+
<div ref={sentinelRef} className="h-px" />
|
| 608 |
+
{projects.isFetchingNextPage && (
|
| 609 |
+
<div className="py-5 text-center font-mono text-xs text-ink-tertiary">Loading more…</div>
|
| 610 |
+
)}
|
| 611 |
+
</>
|
| 612 |
)}
|
| 613 |
+
</div>
|
| 614 |
+
|
| 615 |
+
<div className="mt-4 flex items-center justify-between gap-3 border-t border-line pt-3.5 font-mono text-[10px] uppercase tracking-[0.1em] text-ink-tertiary">
|
| 616 |
+
<span>{(projects.data?.pages[0]?.total ?? 0).toLocaleString()} repositories indexed</span>
|
| 617 |
+
<a href="#top" className="transition-colors hover:text-ink">Back to top ↑</a>
|
| 618 |
+
</div>
|
| 619 |
+
</section>
|
| 620 |
+
</main>
|
| 621 |
|
|
|
|
|
|
|
| 622 |
</div>
|
| 623 |
);
|
| 624 |
}
|
src/features/my/MyProjectsPage.tsx
CHANGED
|
@@ -56,7 +56,7 @@ export function MyProjectsPage() {
|
|
| 56 |
action={
|
| 57 |
<Link
|
| 58 |
to="/submit"
|
| 59 |
-
className="inline-flex h-9 items-center rounded-md bg-
|
| 60 |
>
|
| 61 |
Submit a project
|
| 62 |
</Link>
|
|
|
|
| 56 |
action={
|
| 57 |
<Link
|
| 58 |
to="/submit"
|
| 59 |
+
className="inline-flex h-9 items-center rounded-md bg-ink px-3.5 text-sm font-medium text-surface hover:bg-white"
|
| 60 |
>
|
| 61 |
Submit a project
|
| 62 |
</Link>
|
src/features/project/OwnerFindings.tsx
CHANGED
|
@@ -130,7 +130,7 @@ export function OwnerFindings({
|
|
| 130 |
type="checkbox"
|
| 131 |
checked={allChecked}
|
| 132 |
onChange={toggleAll}
|
| 133 |
-
className="h-4 w-4 rounded border-line accent-[#
|
| 134 |
/>
|
| 135 |
Select undisclosed ({disclosable.length})
|
| 136 |
</label>
|
|
@@ -201,7 +201,7 @@ export function OwnerFindings({
|
|
| 201 |
onClick={(e) => e.stopPropagation()}
|
| 202 |
onChange={() => toggle(f.id)}
|
| 203 |
aria-label={disclosed ? `${f.finding_key} already disclosed` : `Select ${f.finding_key}`}
|
| 204 |
-
className="mt-1 h-4 w-4 shrink-0 rounded border-line accent-[#
|
| 205 |
/>
|
| 206 |
<div className="min-w-0 flex-1">
|
| 207 |
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
|
|
|
| 130 |
type="checkbox"
|
| 131 |
checked={allChecked}
|
| 132 |
onChange={toggleAll}
|
| 133 |
+
className="h-4 w-4 rounded border-line accent-[#EDEFF4]"
|
| 134 |
/>
|
| 135 |
Select undisclosed ({disclosable.length})
|
| 136 |
</label>
|
|
|
|
| 201 |
onClick={(e) => e.stopPropagation()}
|
| 202 |
onChange={() => toggle(f.id)}
|
| 203 |
aria-label={disclosed ? `${f.finding_key} already disclosed` : `Select ${f.finding_key}`}
|
| 204 |
+
className="mt-1 h-4 w-4 shrink-0 rounded border-line accent-[#EDEFF4] disabled:opacity-40"
|
| 205 |
/>
|
| 206 |
<div className="min-w-0 flex-1">
|
| 207 |
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
src/features/project/ProjectPage.tsx
CHANGED
|
@@ -491,13 +491,13 @@ function ScanProgressPage({
|
|
| 491 |
: "VulnHunter is analyzing the repository. Results will appear after the scan completes.";
|
| 492 |
|
| 493 |
return (
|
| 494 |
-
<div className="
|
| 495 |
<div className="openvuln-glow pointer-events-none absolute inset-0 -z-10" />
|
| 496 |
|
| 497 |
<header className="flex items-center justify-between px-5 py-5 sm:px-8 sm:py-7">
|
| 498 |
<Link
|
| 499 |
to="/"
|
| 500 |
-
className="inline-flex items-center gap-2 rounded-full border border-
|
| 501 |
>
|
| 502 |
<ArrowLeft size={14} />
|
| 503 |
OpenVuln
|
|
@@ -507,7 +507,7 @@ function ScanProgressPage({
|
|
| 507 |
href={htmlUrl}
|
| 508 |
target="_blank"
|
| 509 |
rel="noreferrer"
|
| 510 |
-
className="inline-flex items-center gap-2 rounded-full border border-
|
| 511 |
>
|
| 512 |
<Github size={14} />
|
| 513 |
GitHub
|
|
@@ -516,45 +516,45 @@ function ScanProgressPage({
|
|
| 516 |
</header>
|
| 517 |
|
| 518 |
<main className="mx-auto flex min-h-[calc(100vh-88px)] w-full max-w-4xl flex-col items-center justify-center px-5 pb-16 pt-8 text-center sm:px-8">
|
| 519 |
-
<div className="openvuln-brand flex h-16 w-16 items-center justify-center rounded-2xl border border-
|
| 520 |
<LoaderCircle
|
| 521 |
size={28}
|
| 522 |
-
className="animate-spin text-
|
| 523 |
strokeWidth={1.7}
|
| 524 |
/>
|
| 525 |
</div>
|
| 526 |
|
| 527 |
-
<p className="mt-7 font-mono text-[11px] uppercase tracking-[0.22em] text-
|
| 528 |
{justSubmitted ? "Repository added" : "Live scan status"}
|
| 529 |
</p>
|
| 530 |
-
<h1 className="openvuln-title mt-3 text-balance text-3xl font-medium leading-tight sm:text-5xl">
|
| 531 |
{statusLabel}
|
| 532 |
</h1>
|
| 533 |
|
| 534 |
-
<div className="mt-5 flex flex-wrap items-center justify-center gap-2 text-sm text-
|
| 535 |
-
<span className="text-
|
| 536 |
-
<span className="text-
|
| 537 |
-
<span className="text-
|
| 538 |
{branch && (
|
| 539 |
<>
|
| 540 |
-
<span className="text-
|
| 541 |
-
<span className="font-mono text-xs text-
|
| 542 |
</>
|
| 543 |
)}
|
| 544 |
</div>
|
| 545 |
|
| 546 |
-
<div className="openvuln-composer mt-9 w-full max-w-2xl rounded-
|
| 547 |
<div className="flex items-center gap-3">
|
| 548 |
<span className="relative flex h-2.5 w-2.5 shrink-0">
|
| 549 |
-
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-
|
| 550 |
-
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-
|
| 551 |
</span>
|
| 552 |
-
<p className="text-sm font-medium text-
|
| 553 |
</div>
|
| 554 |
-
<p className="mt-3 text-sm leading-6 text-
|
| 555 |
{!loading && (
|
| 556 |
-
<div className="mt-5 border-t border-
|
| 557 |
-
<div className="flex items-center justify-between text-xs text-
|
| 558 |
<span>Current stage</span>
|
| 559 |
<span>
|
| 560 |
{currentStage + 1} of {stages.length}
|
|
@@ -569,20 +569,20 @@ function ScanProgressPage({
|
|
| 569 |
key={stageLabel}
|
| 570 |
className={`flex min-w-0 flex-col items-center gap-2 rounded-xl border px-2 py-3 text-center ${
|
| 571 |
current
|
| 572 |
-
? "border-
|
| 573 |
: complete
|
| 574 |
-
? "border-
|
| 575 |
-
: "border-
|
| 576 |
}`}
|
| 577 |
aria-current={current ? "step" : undefined}
|
| 578 |
>
|
| 579 |
<span
|
| 580 |
className={`flex h-6 w-6 items-center justify-center rounded-full border text-[11px] ${
|
| 581 |
current
|
| 582 |
-
? "border-
|
| 583 |
: complete
|
| 584 |
-
? "border-
|
| 585 |
-
: "border-
|
| 586 |
}`}
|
| 587 |
>
|
| 588 |
{complete ? <Check size={13} strokeWidth={2.2} /> : index + 1}
|
|
@@ -594,7 +594,7 @@ function ScanProgressPage({
|
|
| 594 |
);
|
| 595 |
})}
|
| 596 |
</ol>
|
| 597 |
-
<p className="mt-3 text-xs text-
|
| 598 |
This shows status stages, not elapsed-time progress. Refreshes every 5 seconds.
|
| 599 |
</p>
|
| 600 |
</div>
|
|
|
|
| 491 |
: "VulnHunter is analyzing the repository. Results will appear after the scan completes.";
|
| 492 |
|
| 493 |
return (
|
| 494 |
+
<div className="fixed inset-0 z-50 overflow-y-auto bg-surface text-ink">
|
| 495 |
<div className="openvuln-glow pointer-events-none absolute inset-0 -z-10" />
|
| 496 |
|
| 497 |
<header className="flex items-center justify-between px-5 py-5 sm:px-8 sm:py-7">
|
| 498 |
<Link
|
| 499 |
to="/"
|
| 500 |
+
className="inline-flex items-center gap-2 rounded-full border border-line bg-surface-raised px-3.5 py-2 text-xs font-medium text-ink-secondary transition hover:border-line-strong hover:bg-surface-sunken hover:text-ink focus-ring"
|
| 501 |
>
|
| 502 |
<ArrowLeft size={14} />
|
| 503 |
OpenVuln
|
|
|
|
| 507 |
href={htmlUrl}
|
| 508 |
target="_blank"
|
| 509 |
rel="noreferrer"
|
| 510 |
+
className="inline-flex items-center gap-2 rounded-full border border-line bg-surface-raised px-3.5 py-2 text-xs font-medium text-ink-secondary transition hover:border-line-strong hover:bg-surface-sunken hover:text-ink focus-ring"
|
| 511 |
>
|
| 512 |
<Github size={14} />
|
| 513 |
GitHub
|
|
|
|
| 516 |
</header>
|
| 517 |
|
| 518 |
<main className="mx-auto flex min-h-[calc(100vh-88px)] w-full max-w-4xl flex-col items-center justify-center px-5 pb-16 pt-8 text-center sm:px-8">
|
| 519 |
+
<div className="openvuln-brand flex h-16 w-16 items-center justify-center rounded-2xl border border-line bg-surface-raised shadow-[0_20px_56px_-20px_rgba(0,0,0,0.7)]">
|
| 520 |
<LoaderCircle
|
| 521 |
size={28}
|
| 522 |
+
className="animate-spin text-accent-600 motion-reduce:animate-none"
|
| 523 |
strokeWidth={1.7}
|
| 524 |
/>
|
| 525 |
</div>
|
| 526 |
|
| 527 |
+
<p className="mt-7 font-mono text-[11px] uppercase tracking-[0.22em] text-ink-tertiary">
|
| 528 |
{justSubmitted ? "Repository added" : "Live scan status"}
|
| 529 |
</p>
|
| 530 |
+
<h1 className="openvuln-title mt-3 text-balance font-display text-3xl font-medium leading-tight sm:text-5xl">
|
| 531 |
{statusLabel}
|
| 532 |
</h1>
|
| 533 |
|
| 534 |
+
<div className="mt-5 flex flex-wrap items-center justify-center gap-2 text-sm text-ink-secondary">
|
| 535 |
+
<span className="text-ink">{owner}</span>
|
| 536 |
+
<span className="text-ink-tertiary">/</span>
|
| 537 |
+
<span className="text-ink">{repo}</span>
|
| 538 |
{branch && (
|
| 539 |
<>
|
| 540 |
+
<span className="text-ink-tertiary">·</span>
|
| 541 |
+
<span className="font-mono text-xs text-ink-tertiary">{branch}</span>
|
| 542 |
</>
|
| 543 |
)}
|
| 544 |
</div>
|
| 545 |
|
| 546 |
+
<div className="openvuln-composer mt-9 w-full max-w-2xl rounded-2xl border border-line bg-surface-raised px-5 py-5 text-left shadow-[0_24px_64px_-24px_rgba(0,0,0,0.7)] sm:px-6">
|
| 547 |
<div className="flex items-center gap-3">
|
| 548 |
<span className="relative flex h-2.5 w-2.5 shrink-0">
|
| 549 |
+
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-accent-600 opacity-40 motion-reduce:animate-none" />
|
| 550 |
+
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-accent-600" />
|
| 551 |
</span>
|
| 552 |
+
<p className="text-sm font-medium text-ink">{statusLabel}</p>
|
| 553 |
</div>
|
| 554 |
+
<p className="mt-3 text-sm leading-6 text-ink-secondary">{detail}</p>
|
| 555 |
{!loading && (
|
| 556 |
+
<div className="mt-5 border-t border-line pt-4">
|
| 557 |
+
<div className="flex items-center justify-between text-xs text-ink-tertiary">
|
| 558 |
<span>Current stage</span>
|
| 559 |
<span>
|
| 560 |
{currentStage + 1} of {stages.length}
|
|
|
|
| 569 |
key={stageLabel}
|
| 570 |
className={`flex min-w-0 flex-col items-center gap-2 rounded-xl border px-2 py-3 text-center ${
|
| 571 |
current
|
| 572 |
+
? "border-accent-200 bg-accent-50 text-ink"
|
| 573 |
: complete
|
| 574 |
+
? "border-line bg-surface-sunken text-ink-secondary"
|
| 575 |
+
: "border-line bg-surface text-ink-tertiary"
|
| 576 |
}`}
|
| 577 |
aria-current={current ? "step" : undefined}
|
| 578 |
>
|
| 579 |
<span
|
| 580 |
className={`flex h-6 w-6 items-center justify-center rounded-full border text-[11px] ${
|
| 581 |
current
|
| 582 |
+
? "border-transparent bg-ink text-surface"
|
| 583 |
: complete
|
| 584 |
+
? "border-line bg-surface text-ink-secondary"
|
| 585 |
+
: "border-line text-ink-tertiary"
|
| 586 |
}`}
|
| 587 |
>
|
| 588 |
{complete ? <Check size={13} strokeWidth={2.2} /> : index + 1}
|
|
|
|
| 594 |
);
|
| 595 |
})}
|
| 596 |
</ol>
|
| 597 |
+
<p className="mt-3 text-xs text-ink-tertiary">
|
| 598 |
This shows status stages, not elapsed-time progress. Refreshes every 5 seconds.
|
| 599 |
</p>
|
| 600 |
</div>
|
src/features/project/VersionBar.tsx
CHANGED
|
@@ -83,7 +83,7 @@ export function VersionBar({
|
|
| 83 |
onClick={() => setOpen((v) => !v)}
|
| 84 |
aria-expanded={open}
|
| 85 |
aria-haspopup="menu"
|
| 86 |
-
className="inline-flex h-8 items-center gap-1.5 rounded-full border border-line bg-surface-raised px-3 text-ink transition-colors hover:border-
|
| 87 |
>
|
| 88 |
<CircleCheck size={13} className="text-success" />
|
| 89 |
<span className="font-mono text-[12px]">{viewing?.label ?? "—"}</span>
|
|
|
|
| 83 |
onClick={() => setOpen((v) => !v)}
|
| 84 |
aria-expanded={open}
|
| 85 |
aria-haspopup="menu"
|
| 86 |
+
className="inline-flex h-8 items-center gap-1.5 rounded-full border border-line bg-surface-raised px-3 text-ink transition-colors hover:border-line-strong hover:bg-surface-sunken focus-ring"
|
| 87 |
>
|
| 88 |
<CircleCheck size={13} className="text-success" />
|
| 89 |
<span className="font-mono text-[12px]">{viewing?.label ?? "—"}</span>
|
src/features/submit/SubmitPage.tsx
CHANGED
|
@@ -39,15 +39,15 @@ export function SubmitPage() {
|
|
| 39 |
every submission — only maintainers and admins can start a scan.
|
| 40 |
</p>
|
| 41 |
<a
|
| 42 |
-
href={loginUrl(
|
| 43 |
onClick={(e) => {
|
| 44 |
if (window.top && window.top !== window.self) {
|
| 45 |
e.preventDefault();
|
| 46 |
-
navigateToLogin(
|
| 47 |
window.dispatchEvent(new Event("ov-oauth-popup-opened"));
|
| 48 |
}
|
| 49 |
}}
|
| 50 |
-
className="mt-5 inline-flex h-10 items-center gap-2 rounded-md bg-
|
| 51 |
>
|
| 52 |
<Github size={16} />
|
| 53 |
Sign in with GitHub
|
|
|
|
| 39 |
every submission — only maintainers and admins can start a scan.
|
| 40 |
</p>
|
| 41 |
<a
|
| 42 |
+
href={loginUrl()}
|
| 43 |
onClick={(e) => {
|
| 44 |
if (window.top && window.top !== window.self) {
|
| 45 |
e.preventDefault();
|
| 46 |
+
navigateToLogin();
|
| 47 |
window.dispatchEvent(new Event("ov-oauth-popup-opened"));
|
| 48 |
}
|
| 49 |
}}
|
| 50 |
+
className="mt-5 inline-flex h-10 items-center gap-2 rounded-md bg-ink px-4 text-sm font-medium text-surface transition-colors hover:bg-white focus-ring"
|
| 51 |
>
|
| 52 |
<Github size={16} />
|
| 53 |
Sign in with GitHub
|
src/index.css
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
@tailwind utilities;
|
| 4 |
|
| 5 |
@layer base {
|
| 6 |
-
/*
|
| 7 |
:root {
|
| 8 |
color-scheme: dark;
|
| 9 |
}
|
|
@@ -15,27 +15,29 @@
|
|
| 15 |
}
|
| 16 |
|
| 17 |
select option {
|
| 18 |
-
background-color: #
|
| 19 |
-
color: #
|
| 20 |
}
|
| 21 |
|
| 22 |
body {
|
| 23 |
@apply bg-surface text-ink font-sans;
|
|
|
|
| 24 |
}
|
| 25 |
|
| 26 |
::selection {
|
| 27 |
-
background: rgba(
|
| 28 |
-
color: #
|
| 29 |
}
|
| 30 |
}
|
| 31 |
|
| 32 |
@layer utilities {
|
| 33 |
.focus-ring {
|
| 34 |
-
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/
|
| 35 |
}
|
| 36 |
|
|
|
|
| 37 |
.focus-ring-dark {
|
| 38 |
-
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-
|
| 39 |
}
|
| 40 |
|
| 41 |
.text-balance {
|
|
@@ -44,38 +46,114 @@
|
|
| 44 |
}
|
| 45 |
|
| 46 |
@layer components {
|
| 47 |
-
.openvuln-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
BlinkMacSystemFont,
|
| 51 |
-
"Segoe UI",
|
| 52 |
-
Roboto,
|
| 53 |
-
"Helvetica Neue",
|
| 54 |
-
Arial,
|
| 55 |
-
"Noto Sans",
|
| 56 |
-
sans-serif,
|
| 57 |
-
"Apple Color Emoji",
|
| 58 |
-
"Segoe UI Emoji",
|
| 59 |
-
"Segoe UI Symbol",
|
| 60 |
-
"Noto Color Emoji";
|
| 61 |
}
|
| 62 |
|
| 63 |
-
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
| 65 |
}
|
| 66 |
|
| 67 |
-
.openvuln-
|
| 68 |
-
|
| 69 |
}
|
| 70 |
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
content: "";
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
}
|
| 80 |
|
| 81 |
.openvuln-brand {
|
|
@@ -87,31 +165,40 @@
|
|
| 87 |
}
|
| 88 |
|
| 89 |
.openvuln-panel {
|
| 90 |
-
border: 1px solid
|
| 91 |
-
border-radius:
|
| 92 |
-
background:
|
| 93 |
-
|
| 94 |
-
rgba(16, 17, 21, 0.82);
|
| 95 |
-
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.22);
|
| 96 |
-
backdrop-filter: blur(18px);
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
.openvuln-scrollbar {
|
| 100 |
-
scrollbar-width: thin;
|
| 101 |
-
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
|
| 102 |
}
|
| 103 |
|
| 104 |
-
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
}
|
| 107 |
|
| 108 |
-
.openvuln-
|
| 109 |
-
|
|
|
|
|
|
|
| 110 |
}
|
| 111 |
|
| 112 |
-
.openvuln-
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
}
|
| 116 |
}
|
| 117 |
|
|
@@ -149,11 +236,30 @@
|
|
| 149 |
animation: none;
|
| 150 |
transform: translateX(200%);
|
| 151 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
}
|
| 153 |
|
| 154 |
body.openvuln-running {
|
| 155 |
overflow: hidden;
|
| 156 |
-
background: #
|
| 157 |
}
|
| 158 |
|
| 159 |
body.openvuln-running .app-shell-chrome {
|
|
|
|
| 3 |
@tailwind utilities;
|
| 4 |
|
| 5 |
@layer base {
|
| 6 |
+
/* 暗色控制台体系:原生控件(select 弹层/滚动条/checkbox)跟随暗色 */
|
| 7 |
:root {
|
| 8 |
color-scheme: dark;
|
| 9 |
}
|
|
|
|
| 15 |
}
|
| 16 |
|
| 17 |
select option {
|
| 18 |
+
background-color: #12151c;
|
| 19 |
+
color: #edeff4;
|
| 20 |
}
|
| 21 |
|
| 22 |
body {
|
| 23 |
@apply bg-surface text-ink font-sans;
|
| 24 |
+
font-feature-settings: "cv05" 1, "ss01" 1;
|
| 25 |
}
|
| 26 |
|
| 27 |
::selection {
|
| 28 |
+
background: rgba(237, 239, 244, 0.2);
|
| 29 |
+
color: #ffffff;
|
| 30 |
}
|
| 31 |
}
|
| 32 |
|
| 33 |
@layer utilities {
|
| 34 |
.focus-ring {
|
| 35 |
+
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-[#0C0E13];
|
| 36 |
}
|
| 37 |
|
| 38 |
+
/* 历史命名保留:与 .focus-ring 一致 */
|
| 39 |
.focus-ring-dark {
|
| 40 |
+
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-[#0C0E13];
|
| 41 |
}
|
| 42 |
|
| 43 |
.text-balance {
|
|
|
|
| 46 |
}
|
| 47 |
|
| 48 |
@layer components {
|
| 49 |
+
.openvuln-title {
|
| 50 |
+
color: #edeff4;
|
| 51 |
+
letter-spacing: -0.02em;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
}
|
| 53 |
|
| 54 |
+
/* 大字报标题:第一行空心描边(hover 时微微填充) */
|
| 55 |
+
.openvuln-outline {
|
| 56 |
+
color: transparent;
|
| 57 |
+
-webkit-text-stroke: 1.5px rgba(237, 239, 244, 0.56);
|
| 58 |
+
transition: color 500ms cubic-bezier(0.22, 1, 0.36, 1);
|
| 59 |
}
|
| 60 |
|
| 61 |
+
h1:hover .openvuln-outline {
|
| 62 |
+
color: rgba(237, 239, 244, 0.12);
|
| 63 |
}
|
| 64 |
|
| 65 |
+
@supports not ((-webkit-text-stroke: 1px white)) {
|
| 66 |
+
.openvuln-outline {
|
| 67 |
+
color: rgba(237, 239, 244, 0.56);
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
/* 大字报标题整行印刷式抬升入场 */
|
| 72 |
+
.ov-line-mask {
|
| 73 |
+
display: block;
|
| 74 |
+
overflow: hidden;
|
| 75 |
+
padding-bottom: 0.12em;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.ov-line {
|
| 79 |
+
display: block;
|
| 80 |
+
transform: translateY(112%);
|
| 81 |
+
animation: ov-line-up 850ms cubic-bezier(0.19, 1, 0.22, 1) forwards;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.openvuln-title-mark {
|
| 85 |
+
position: relative;
|
| 86 |
+
display: inline-block;
|
| 87 |
+
-webkit-text-stroke: 0;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.openvuln-title-comma {
|
| 91 |
+
display: inline-block;
|
| 92 |
+
margin-left: 4px;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.openvuln-title-period {
|
| 96 |
+
width: 0.1em;
|
| 97 |
+
height: 0.1em;
|
| 98 |
+
margin-left: 0.1em;
|
| 99 |
+
border-radius: 999px;
|
| 100 |
+
background: rgba(237, 239, 244, 0.88);
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
@keyframes ov-line-up {
|
| 104 |
+
to {
|
| 105 |
+
transform: translateY(0);
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
/* 滚动进入视口的克制上浮 */
|
| 110 |
+
.ov-reveal {
|
| 111 |
+
opacity: 0;
|
| 112 |
+
transform: translateY(14px);
|
| 113 |
+
transition:
|
| 114 |
+
opacity 420ms cubic-bezier(0.22, 1, 0.36, 1),
|
| 115 |
+
transform 420ms cubic-bezier(0.22, 1, 0.36, 1);
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.ov-reveal.ov-in {
|
| 119 |
+
opacity: 1;
|
| 120 |
+
transform: none;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
/* 顶栏导航的下划线扫入 */
|
| 124 |
+
.ov-navlink {
|
| 125 |
+
position: relative;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.ov-navlink::after {
|
| 129 |
content: "";
|
| 130 |
+
position: absolute;
|
| 131 |
+
left: 0;
|
| 132 |
+
right: 0;
|
| 133 |
+
bottom: -4px;
|
| 134 |
+
height: 1px;
|
| 135 |
+
background: #edeff4;
|
| 136 |
+
transform: scaleX(0);
|
| 137 |
+
transform-origin: left;
|
| 138 |
+
transition: transform 260ms cubic-bezier(0.22, 1, 0.36, 1);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.ov-navlink:hover::after,
|
| 142 |
+
.ov-navlink:focus-visible::after {
|
| 143 |
+
transform: scaleX(1);
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
/* 大字报标题:第二行实心,带极轻的金属纵向光泽 */
|
| 147 |
+
.openvuln-solid {
|
| 148 |
+
background: linear-gradient(180deg, #ffffff 0%, #eceef2 52%, #c2c8d2 100%);
|
| 149 |
+
background-clip: text;
|
| 150 |
+
-webkit-background-clip: text;
|
| 151 |
+
color: transparent;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
/* 极淡的顶部纵深,用于扫描进度页 */
|
| 155 |
+
.openvuln-glow {
|
| 156 |
+
background: radial-gradient(ellipse 52% 30% at 50% 0%, rgba(237, 239, 244, 0.04), transparent 70%);
|
| 157 |
}
|
| 158 |
|
| 159 |
.openvuln-brand {
|
|
|
|
| 165 |
}
|
| 166 |
|
| 167 |
.openvuln-panel {
|
| 168 |
+
border: 1px solid #232833;
|
| 169 |
+
border-radius: 16px;
|
| 170 |
+
background: #12151c;
|
| 171 |
+
box-shadow: 0 20px 48px -24px rgba(0, 0, 0, 0.6);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
}
|
| 173 |
|
| 174 |
+
/* hero 背景视频:原生暗色线网动画,向下淡入页面底色 */
|
| 175 |
+
.openvuln-hero-video {
|
| 176 |
+
position: absolute;
|
| 177 |
+
inset: 0 0 auto 0;
|
| 178 |
+
z-index: -10;
|
| 179 |
+
height: 720px;
|
| 180 |
+
overflow: hidden;
|
| 181 |
+
pointer-events: none;
|
| 182 |
}
|
| 183 |
|
| 184 |
+
.openvuln-hero-video video {
|
| 185 |
+
height: 100%;
|
| 186 |
+
width: 100%;
|
| 187 |
+
object-fit: cover;
|
| 188 |
}
|
| 189 |
|
| 190 |
+
.openvuln-hero-video::after {
|
| 191 |
+
content: "";
|
| 192 |
+
position: absolute;
|
| 193 |
+
inset: 0;
|
| 194 |
+
background: linear-gradient(
|
| 195 |
+
to bottom,
|
| 196 |
+
rgba(12, 14, 19, 0.38) 0%,
|
| 197 |
+
rgba(12, 14, 19, 0.18) 34%,
|
| 198 |
+
rgba(12, 14, 19, 0.55) 62%,
|
| 199 |
+
rgba(12, 14, 19, 0.94) 86%,
|
| 200 |
+
#0c0e13 100%
|
| 201 |
+
);
|
| 202 |
}
|
| 203 |
}
|
| 204 |
|
|
|
|
| 236 |
animation: none;
|
| 237 |
transform: translateX(200%);
|
| 238 |
}
|
| 239 |
+
|
| 240 |
+
.openvuln-hero-video video {
|
| 241 |
+
display: none;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.ov-line {
|
| 245 |
+
animation: none;
|
| 246 |
+
transform: none;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
.ov-reveal {
|
| 250 |
+
opacity: 1;
|
| 251 |
+
transform: none;
|
| 252 |
+
transition: none;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
.ov-navlink::after {
|
| 256 |
+
transition: none;
|
| 257 |
+
}
|
| 258 |
}
|
| 259 |
|
| 260 |
body.openvuln-running {
|
| 261 |
overflow: hidden;
|
| 262 |
+
background: #0c0e13;
|
| 263 |
}
|
| 264 |
|
| 265 |
body.openvuln-running .app-shell-chrome {
|
src/main.tsx
CHANGED
|
@@ -1,5 +1,10 @@
|
|
| 1 |
import { StrictMode } from "react";
|
| 2 |
import { createRoot } from "react-dom/client";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import "./index.css";
|
| 4 |
import { App } from "./App";
|
| 5 |
|
|
|
|
| 1 |
import { StrictMode } from "react";
|
| 2 |
import { createRoot } from "react-dom/client";
|
| 3 |
+
import "@fontsource-variable/inter";
|
| 4 |
+
import "@fontsource-variable/space-grotesk";
|
| 5 |
+
import "@fontsource/ibm-plex-mono/400.css";
|
| 6 |
+
import "@fontsource/ibm-plex-mono/500.css";
|
| 7 |
+
import "@fontsource/ibm-plex-mono/600.css";
|
| 8 |
import "./index.css";
|
| 9 |
import { App } from "./App";
|
| 10 |
|
src/shared/api/client.ts
CHANGED
|
@@ -76,7 +76,7 @@ export interface MeResponse {
|
|
| 76 |
}
|
| 77 |
|
| 78 |
/** GitHub OAuth login URL (full-page redirect). */
|
| 79 |
-
export function loginUrl(returnTo: string): string {
|
| 80 |
// 站内相对路径与白名单绝对地址(HF 部署回跳)都放行 —— 白名单校验在后端;
|
| 81 |
// 仅协议相对 "//…" 一律拒绝(开放重定向的唯一真风险)。
|
| 82 |
const safe = returnTo.startsWith("//") ? "/" : returnTo;
|
|
|
|
| 76 |
}
|
| 77 |
|
| 78 |
/** GitHub OAuth login URL (full-page redirect). */
|
| 79 |
+
export function loginUrl(returnTo: string = currentReturnTo()): string {
|
| 80 |
// 站内相对路径与白名单绝对地址(HF 部署回跳)都放行 —— 白名单校验在后端;
|
| 81 |
// 仅协议相对 "//…" 一律拒绝(开放重定向的唯一真风险)。
|
| 82 |
const safe = returnTo.startsWith("//") ? "/" : returnTo;
|
tailwind.config.js
CHANGED
|
@@ -4,48 +4,50 @@ export default {
|
|
| 4 |
theme: {
|
| 5 |
extend: {
|
| 6 |
colors: {
|
| 7 |
-
// ──
|
| 8 |
surface: {
|
| 9 |
-
DEFAULT: "#
|
| 10 |
-
raised: "#
|
| 11 |
-
sunken: "#
|
| 12 |
-
header: "#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
},
|
| 14 |
-
line: "#26272c", // 分隔线/边框(#26272c ~ #2f3037)
|
| 15 |
ink: {
|
| 16 |
-
DEFAULT: "#
|
| 17 |
-
secondary: "#
|
| 18 |
-
tertiary: "#
|
| 19 |
},
|
| 20 |
-
// 交互
|
| 21 |
accent: {
|
| 22 |
-
50: "rgba(
|
| 23 |
-
100: "rgba(
|
| 24 |
-
200: "rgba(
|
| 25 |
-
600: "#
|
| 26 |
-
700: "#
|
| 27 |
-
800: "#
|
| 28 |
},
|
| 29 |
-
// 功能色
|
| 30 |
sev: {
|
| 31 |
-
critical: { ink: "#
|
| 32 |
-
high: { ink: "#
|
| 33 |
-
medium: { ink: "#
|
| 34 |
-
low: { ink: "#
|
| 35 |
// info:VH 兜底档,公众 UI 不展示,仅防御保留
|
| 36 |
-
info: { ink: "#
|
| 37 |
},
|
| 38 |
-
ai: { DEFAULT: "#9285FF", bg: "rgba(146, 133, 255, 0.
|
| 39 |
-
success: { DEFAULT: "#
|
| 40 |
-
danger: "#
|
| 41 |
-
warning: "#
|
| 42 |
-
running: { DEFAULT: "#
|
| 43 |
},
|
| 44 |
fontFamily: {
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
mono: ["ui-monospace", "SFMono-Regular", "Menlo", "Consolas", "monospace"],
|
| 49 |
},
|
| 50 |
maxWidth: {
|
| 51 |
"6xl": "72rem",
|
|
|
|
| 4 |
theme: {
|
| 5 |
extend: {
|
| 6 |
colors: {
|
| 7 |
+
// ── 暗色控制台视觉语言:石墨蓝黑分层,彩色只留给风险数据 ──
|
| 8 |
surface: {
|
| 9 |
+
DEFAULT: "#0C0E13", // 页面底(void)
|
| 10 |
+
raised: "#12151C", // 卡片/面板(graphite)
|
| 11 |
+
sunken: "#191D26", // 井/hover 底
|
| 12 |
+
header: "#0C0E13", // 顶栏
|
| 13 |
+
},
|
| 14 |
+
line: {
|
| 15 |
+
DEFAULT: "#232833", // hairline 分隔线/边框
|
| 16 |
+
strong: "#37404F", // hover 边框
|
| 17 |
},
|
|
|
|
| 18 |
ink: {
|
| 19 |
+
DEFAULT: "#EDEFF4", // 主文字(暖白)
|
| 20 |
+
secondary: "#9BA3B0", // 次文字
|
| 21 |
+
tertiary: "#646C7A", // 辅助/placeholder
|
| 22 |
},
|
| 23 |
+
// 交互单色(phosphor 白):链接/聚焦/进行中一律无彩,彩色专属数据
|
| 24 |
accent: {
|
| 25 |
+
50: "rgba(237, 239, 244, 0.05)",
|
| 26 |
+
100: "rgba(237, 239, 244, 0.09)",
|
| 27 |
+
200: "rgba(237, 239, 244, 0.18)",
|
| 28 |
+
600: "#D7DBE2",
|
| 29 |
+
700: "#FFFFFF",
|
| 30 |
+
800: "#FFFFFF",
|
| 31 |
},
|
| 32 |
+
// 功能色(数据语义专用):NVD 四档 红→橙→琥珀
|
| 33 |
sev: {
|
| 34 |
+
critical: { ink: "#FF7A72", bg: "rgba(229, 72, 77, 0.14)", bar: "#E5484D" },
|
| 35 |
+
high: { ink: "#FF9A8A", bg: "rgba(242, 96, 79, 0.12)", bar: "#F2604F" },
|
| 36 |
+
medium: { ink: "#FFB37E", bg: "rgba(255, 138, 60, 0.12)", bar: "#FF8A3C" },
|
| 37 |
+
low: { ink: "#F2CE60", bg: "rgba(240, 196, 58, 0.12)", bar: "#F0C43A" },
|
| 38 |
// info:VH 兜底档,公众 UI 不展示,仅防御保留
|
| 39 |
+
info: { ink: "#9BA3B0", bg: "rgba(155, 163, 176, 0.10)", bar: "#646C7A" },
|
| 40 |
},
|
| 41 |
+
ai: { DEFAULT: "#9285FF", bg: "rgba(146, 133, 255, 0.12)", ink: "#A79DFF" },
|
| 42 |
+
success: { DEFAULT: "#34B87C", bg: "rgba(52, 184, 124, 0.12)", ink: "#57D69C" },
|
| 43 |
+
danger: "#F0655F",
|
| 44 |
+
warning: "#FF8A3C",
|
| 45 |
+
running: { DEFAULT: "#D7DBE2", bg: "rgba(237, 239, 244, 0.08)", ink: "#D7DBE2" },
|
| 46 |
},
|
| 47 |
fontFamily: {
|
| 48 |
+
display: ['"Space Grotesk Variable"', '"Inter Variable"', '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', '"PingFang SC"', '"Microsoft YaHei"', 'sans-serif'],
|
| 49 |
+
sans: ['"Inter Variable"', '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', '"Helvetica Neue"', 'Arial', '"Noto Sans"', '"PingFang SC"', '"Microsoft YaHei"', 'sans-serif'],
|
| 50 |
+
mono: ['"IBM Plex Mono"', 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Consolas', 'monospace'],
|
|
|
|
| 51 |
},
|
| 52 |
maxWidth: {
|
| 53 |
"6xl": "72rem",
|