From 300c840509f55d08e1ce3e5e769e1519debf3c05 Mon Sep 17 00:00:00 2001 From: hao Date: Tue, 17 Mar 2026 01:22:36 -0700 Subject: [PATCH] Vendorize Lovart dashboard shell --- 07-framework-security/cms/directus/INDEX.md | 2 +- 07-framework-security/cms/discourse/INDEX.md | 2 +- 07-framework-security/cms/drupal/INDEX.md | 2 +- 07-framework-security/cms/ghost/INDEX.md | 2 +- 07-framework-security/cms/joomla/INDEX.md | 2 +- 07-framework-security/cms/mediawiki/INDEX.md | 2 +- 07-framework-security/cms/moodle/INDEX.md | 2 +- 07-framework-security/cms/strapi/INDEX.md | 2 +- 07-framework-security/cms/wordpress/INDEX.md | 2 +- .../ecommerce/adobe-commerce/INDEX.md | 2 +- .../ecommerce/magento-open-source/INDEX.md | 2 +- .../ecommerce/medusa/INDEX.md | 2 +- .../ecommerce/opencart/INDEX.md | 2 +- .../ecommerce/openmage/INDEX.md | 2 +- .../ecommerce/prestashop/INDEX.md | 2 +- .../ecommerce/saleor/INDEX.md | 2 +- .../ecommerce/shopware/INDEX.md | 2 +- .../ecommerce/woocommerce/INDEX.md | 2 +- .../frameworks/angular/INDEX.md | 2 +- .../frameworks/aspnet-core/INDEX.md | 2 +- .../frameworks/astro/INDEX.md | 2 +- .../frameworks/django/INDEX.md | 2 +- .../frameworks/echo/INDEX.md | 2 +- .../frameworks/esbuild/INDEX.md | 2 +- .../frameworks/express/INDEX.md | 2 +- .../frameworks/fastify/INDEX.md | 2 +- .../frameworks/flask/INDEX.md | 2 +- 07-framework-security/frameworks/gin/INDEX.md | 2 +- .../frameworks/hapi/INDEX.md | 2 +- 07-framework-security/frameworks/koa/INDEX.md | 2 +- .../frameworks/laravel/INDEX.md | 2 +- .../frameworks/nestjs/INDEX.md | 2 +- .../frameworks/nextjs/INDEX.md | 2 +- .../frameworks/nodejs/INDEX.md | 2 +- .../frameworks/nuxt/INDEX.md | 2 +- .../frameworks/rails/INDEX.md | 2 +- .../frameworks/react/INDEX.md | 2 +- .../frameworks/spring-boot/INDEX.md | 2 +- .../frameworks/spring-framework/INDEX.md | 2 +- .../frameworks/spring-security/INDEX.md | 2 +- .../frameworks/sveltekit/INDEX.md | 2 +- .../frameworks/symfony/INDEX.md | 2 +- .../frameworks/undici/INDEX.md | 2 +- .../frameworks/vite/INDEX.md | 2 +- 07-framework-security/frameworks/vue/INDEX.md | 2 +- .../frameworks/webpack/INDEX.md | 2 +- .../frameworks/werkzeug/INDEX.md | 2 +- .../platforms/adminer/INDEX.md | 2 +- .../platforms/gitea/INDEX.md | 2 +- .../platforms/gitlab-ce/INDEX.md | 2 +- .../platforms/grafana/INDEX.md | 2 +- .../platforms/jenkins/INDEX.md | 2 +- .../platforms/kibana/INDEX.md | 2 +- .../platforms/mattermost/INDEX.md | 2 +- .../platforms/phpmyadmin/INDEX.md | 2 +- .../platforms/redmine/INDEX.md | 2 +- .../servers/apache-httpd/INDEX.md | 2 +- .../servers/apache-tomcat/INDEX.md | 2 +- 07-framework-security/servers/caddy/INDEX.md | 2 +- .../servers/haproxy/INDEX.md | 2 +- 07-framework-security/servers/nginx/INDEX.md | 2 +- .../servers/traefik/INDEX.md | 2 +- .../generated/dashboard/assets/app.js | 911 +++++----- .../dashboard/assets/design-source.json | 18 + .../generated/dashboard/assets/icons.svg | 74 + .../generated/dashboard/assets/styles.css | 1289 +++++++++----- .../dashboard/docs/design-source.html | 111 ++ .../docs/frontend-dashboard-design.html | 23 +- .../dashboard/docs/project-features.html | 14 +- .../dashboard/docs/secure-code-index.html | 2 +- .../generated/dashboard/index.html | 175 +- .../dashboard/legacy/advisories.json | 1 + .../generated/dashboard/legacy/assets/app.js | 573 ++++++ .../dashboard/legacy/assets/styles.css | 728 ++++++++ .../generated/dashboard/legacy/docs | 1 + .../generated/dashboard/legacy/index.html | 81 + .../generated/dashboard/legacy/profiles.json | 1 + .../generated/dashboard/legacy/runs | 1 + .../generated/dashboard/legacy/runs.json | 1 + .../generated/dashboard/legacy/summary.json | 1 + .../generated/dashboard/legacy/systems.json | 1 + .../generated/dashboard/summary.json | 2 +- 08-threat-intel/generated/latest-ingest.md | 2 +- 08-threat-intel/generated/run-summary.json | 2 +- README.md | 9 + docs/frontend-dashboard-design.md | 21 +- docs/project-features.md | 12 + scripts/intel/validators.py | 30 + .../dashboard_templates/lovart/assets/app.js | 708 ++++++++ scripts/lab/render.py | 1542 ++--------------- 90 files changed, 4046 insertions(+), 2412 deletions(-) create mode 100644 08-threat-intel/generated/dashboard/assets/design-source.json create mode 100644 08-threat-intel/generated/dashboard/assets/icons.svg create mode 100644 08-threat-intel/generated/dashboard/docs/design-source.html create mode 120000 08-threat-intel/generated/dashboard/legacy/advisories.json create mode 100644 08-threat-intel/generated/dashboard/legacy/assets/app.js create mode 100644 08-threat-intel/generated/dashboard/legacy/assets/styles.css create mode 120000 08-threat-intel/generated/dashboard/legacy/docs create mode 100644 08-threat-intel/generated/dashboard/legacy/index.html create mode 120000 08-threat-intel/generated/dashboard/legacy/profiles.json create mode 120000 08-threat-intel/generated/dashboard/legacy/runs create mode 120000 08-threat-intel/generated/dashboard/legacy/runs.json create mode 120000 08-threat-intel/generated/dashboard/legacy/summary.json create mode 120000 08-threat-intel/generated/dashboard/legacy/systems.json create mode 100644 scripts/lab/dashboard_templates/lovart/assets/app.js diff --git a/07-framework-security/cms/directus/INDEX.md b/07-framework-security/cms/directus/INDEX.md index 1e94e0b3..3490b6e8 100644 --- a/07-framework-security/cms/directus/INDEX.md +++ b/07-framework-security/cms/directus/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:56+00:00` +- 最近渲染时间: `2026-03-17T08:20:15+00:00` ## 目标约束 diff --git a/07-framework-security/cms/discourse/INDEX.md b/07-framework-security/cms/discourse/INDEX.md index 54893fa6..6d268c57 100644 --- a/07-framework-security/cms/discourse/INDEX.md +++ b/07-framework-security/cms/discourse/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:56+00:00` +- 最近渲染时间: `2026-03-17T08:20:15+00:00` ## 目标约束 diff --git a/07-framework-security/cms/drupal/INDEX.md b/07-framework-security/cms/drupal/INDEX.md index 9f0e2a67..aa0b2c13 100644 --- a/07-framework-security/cms/drupal/INDEX.md +++ b/07-framework-security/cms/drupal/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:56+00:00` +- 最近渲染时间: `2026-03-17T08:20:15+00:00` ## 目标约束 diff --git a/07-framework-security/cms/ghost/INDEX.md b/07-framework-security/cms/ghost/INDEX.md index 1f2e365f..a294a0c3 100644 --- a/07-framework-security/cms/ghost/INDEX.md +++ b/07-framework-security/cms/ghost/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:56+00:00` +- 最近渲染时间: `2026-03-17T08:20:15+00:00` ## 目标约束 diff --git a/07-framework-security/cms/joomla/INDEX.md b/07-framework-security/cms/joomla/INDEX.md index 2a6fcf7b..149c7c1d 100644 --- a/07-framework-security/cms/joomla/INDEX.md +++ b/07-framework-security/cms/joomla/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:56+00:00` +- 最近渲染时间: `2026-03-17T08:20:15+00:00` ## 目标约束 diff --git a/07-framework-security/cms/mediawiki/INDEX.md b/07-framework-security/cms/mediawiki/INDEX.md index d48489f3..02a524b2 100644 --- a/07-framework-security/cms/mediawiki/INDEX.md +++ b/07-framework-security/cms/mediawiki/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:56+00:00` +- 最近渲染时间: `2026-03-17T08:20:15+00:00` ## 目标约束 diff --git a/07-framework-security/cms/moodle/INDEX.md b/07-framework-security/cms/moodle/INDEX.md index 435c512d..cf03ae02 100644 --- a/07-framework-security/cms/moodle/INDEX.md +++ b/07-framework-security/cms/moodle/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:56+00:00` +- 最近渲染时间: `2026-03-17T08:20:15+00:00` ## 目标约束 diff --git a/07-framework-security/cms/strapi/INDEX.md b/07-framework-security/cms/strapi/INDEX.md index 6c3826bf..96fa9770 100644 --- a/07-framework-security/cms/strapi/INDEX.md +++ b/07-framework-security/cms/strapi/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:56+00:00` +- 最近渲染时间: `2026-03-17T08:20:15+00:00` ## 目标约束 diff --git a/07-framework-security/cms/wordpress/INDEX.md b/07-framework-security/cms/wordpress/INDEX.md index a2d1bfec..041e7478 100644 --- a/07-framework-security/cms/wordpress/INDEX.md +++ b/07-framework-security/cms/wordpress/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:56+00:00` +- 最近渲染时间: `2026-03-17T08:20:15+00:00` ## 目标约束 diff --git a/07-framework-security/ecommerce/adobe-commerce/INDEX.md b/07-framework-security/ecommerce/adobe-commerce/INDEX.md index ad036c19..0399713a 100644 --- a/07-framework-security/ecommerce/adobe-commerce/INDEX.md +++ b/07-framework-security/ecommerce/adobe-commerce/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:56+00:00` +- 最近渲染时间: `2026-03-17T08:20:15+00:00` ## 目标约束 diff --git a/07-framework-security/ecommerce/magento-open-source/INDEX.md b/07-framework-security/ecommerce/magento-open-source/INDEX.md index 515d3d0e..f6224d16 100644 --- a/07-framework-security/ecommerce/magento-open-source/INDEX.md +++ b/07-framework-security/ecommerce/magento-open-source/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:56+00:00` +- 最近渲染时间: `2026-03-17T08:20:15+00:00` ## 目标约束 diff --git a/07-framework-security/ecommerce/medusa/INDEX.md b/07-framework-security/ecommerce/medusa/INDEX.md index 38e85c8f..29fe9e6c 100644 --- a/07-framework-security/ecommerce/medusa/INDEX.md +++ b/07-framework-security/ecommerce/medusa/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:56+00:00` +- 最近渲染时间: `2026-03-17T08:20:15+00:00` ## 目标约束 diff --git a/07-framework-security/ecommerce/opencart/INDEX.md b/07-framework-security/ecommerce/opencart/INDEX.md index a51fa75c..070d0031 100644 --- a/07-framework-security/ecommerce/opencart/INDEX.md +++ b/07-framework-security/ecommerce/opencart/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:56+00:00` +- 最近渲染时间: `2026-03-17T08:20:15+00:00` ## 目标约束 diff --git a/07-framework-security/ecommerce/openmage/INDEX.md b/07-framework-security/ecommerce/openmage/INDEX.md index 26db064f..86885d0b 100644 --- a/07-framework-security/ecommerce/openmage/INDEX.md +++ b/07-framework-security/ecommerce/openmage/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:56+00:00` +- 最近渲染时间: `2026-03-17T08:20:15+00:00` ## 目标约束 diff --git a/07-framework-security/ecommerce/prestashop/INDEX.md b/07-framework-security/ecommerce/prestashop/INDEX.md index cebb88e2..fe8b5624 100644 --- a/07-framework-security/ecommerce/prestashop/INDEX.md +++ b/07-framework-security/ecommerce/prestashop/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:56+00:00` +- 最近渲染时间: `2026-03-17T08:20:15+00:00` ## 目标约束 diff --git a/07-framework-security/ecommerce/saleor/INDEX.md b/07-framework-security/ecommerce/saleor/INDEX.md index 651a17a0..e14944c3 100644 --- a/07-framework-security/ecommerce/saleor/INDEX.md +++ b/07-framework-security/ecommerce/saleor/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:56+00:00` +- 最近渲染时间: `2026-03-17T08:20:15+00:00` ## 目标约束 diff --git a/07-framework-security/ecommerce/shopware/INDEX.md b/07-framework-security/ecommerce/shopware/INDEX.md index c172c541..12a5ebb5 100644 --- a/07-framework-security/ecommerce/shopware/INDEX.md +++ b/07-framework-security/ecommerce/shopware/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:56+00:00` +- 最近渲染时间: `2026-03-17T08:20:15+00:00` ## 目标约束 diff --git a/07-framework-security/ecommerce/woocommerce/INDEX.md b/07-framework-security/ecommerce/woocommerce/INDEX.md index 2ced713a..6623069f 100644 --- a/07-framework-security/ecommerce/woocommerce/INDEX.md +++ b/07-framework-security/ecommerce/woocommerce/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:56+00:00` +- 最近渲染时间: `2026-03-17T08:20:15+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/angular/INDEX.md b/07-framework-security/frameworks/angular/INDEX.md index dcf7c4a5..ba9f7fed 100644 --- a/07-framework-security/frameworks/angular/INDEX.md +++ b/07-framework-security/frameworks/angular/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:58+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/aspnet-core/INDEX.md b/07-framework-security/frameworks/aspnet-core/INDEX.md index c9a73a04..38210cd9 100644 --- a/07-framework-security/frameworks/aspnet-core/INDEX.md +++ b/07-framework-security/frameworks/aspnet-core/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:59+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/astro/INDEX.md b/07-framework-security/frameworks/astro/INDEX.md index 7dc5a5af..a840ff53 100644 --- a/07-framework-security/frameworks/astro/INDEX.md +++ b/07-framework-security/frameworks/astro/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:58+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/django/INDEX.md b/07-framework-security/frameworks/django/INDEX.md index b6f83ad6..a86f315e 100644 --- a/07-framework-security/frameworks/django/INDEX.md +++ b/07-framework-security/frameworks/django/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:59+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/echo/INDEX.md b/07-framework-security/frameworks/echo/INDEX.md index 9eba3112..c43c8155 100644 --- a/07-framework-security/frameworks/echo/INDEX.md +++ b/07-framework-security/frameworks/echo/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:59+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/esbuild/INDEX.md b/07-framework-security/frameworks/esbuild/INDEX.md index 4d9280ce..84a0bc67 100644 --- a/07-framework-security/frameworks/esbuild/INDEX.md +++ b/07-framework-security/frameworks/esbuild/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:59+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/express/INDEX.md b/07-framework-security/frameworks/express/INDEX.md index 60463d4a..0aaf8678 100644 --- a/07-framework-security/frameworks/express/INDEX.md +++ b/07-framework-security/frameworks/express/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:58+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/fastify/INDEX.md b/07-framework-security/frameworks/fastify/INDEX.md index 60c2bcf7..fd18c9fb 100644 --- a/07-framework-security/frameworks/fastify/INDEX.md +++ b/07-framework-security/frameworks/fastify/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:58+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/flask/INDEX.md b/07-framework-security/frameworks/flask/INDEX.md index f60106bc..4580664c 100644 --- a/07-framework-security/frameworks/flask/INDEX.md +++ b/07-framework-security/frameworks/flask/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:59+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/gin/INDEX.md b/07-framework-security/frameworks/gin/INDEX.md index ae870af9..cb9b75bc 100644 --- a/07-framework-security/frameworks/gin/INDEX.md +++ b/07-framework-security/frameworks/gin/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:59+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/hapi/INDEX.md b/07-framework-security/frameworks/hapi/INDEX.md index 9df23faf..8e087fe6 100644 --- a/07-framework-security/frameworks/hapi/INDEX.md +++ b/07-framework-security/frameworks/hapi/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:58+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/koa/INDEX.md b/07-framework-security/frameworks/koa/INDEX.md index 090bc378..990ed7a3 100644 --- a/07-framework-security/frameworks/koa/INDEX.md +++ b/07-framework-security/frameworks/koa/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:58+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/laravel/INDEX.md b/07-framework-security/frameworks/laravel/INDEX.md index e53702f1..43c0e8b0 100644 --- a/07-framework-security/frameworks/laravel/INDEX.md +++ b/07-framework-security/frameworks/laravel/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:59+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/nestjs/INDEX.md b/07-framework-security/frameworks/nestjs/INDEX.md index bc1b3d9e..ae20cd96 100644 --- a/07-framework-security/frameworks/nestjs/INDEX.md +++ b/07-framework-security/frameworks/nestjs/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:58+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/nextjs/INDEX.md b/07-framework-security/frameworks/nextjs/INDEX.md index 9248601f..e9632cac 100644 --- a/07-framework-security/frameworks/nextjs/INDEX.md +++ b/07-framework-security/frameworks/nextjs/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `26` -- 最近渲染时间: `2026-03-17T07:35:57+00:00` +- 最近渲染时间: `2026-03-17T08:20:16+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/nodejs/INDEX.md b/07-framework-security/frameworks/nodejs/INDEX.md index 0efe2fb9..7a04e1e0 100644 --- a/07-framework-security/frameworks/nodejs/INDEX.md +++ b/07-framework-security/frameworks/nodejs/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:58+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/nuxt/INDEX.md b/07-framework-security/frameworks/nuxt/INDEX.md index 0eefd22d..a4f3f092 100644 --- a/07-framework-security/frameworks/nuxt/INDEX.md +++ b/07-framework-security/frameworks/nuxt/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:57+00:00` +- 最近渲染时间: `2026-03-17T08:20:16+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/rails/INDEX.md b/07-framework-security/frameworks/rails/INDEX.md index 97603448..f8bf34e4 100644 --- a/07-framework-security/frameworks/rails/INDEX.md +++ b/07-framework-security/frameworks/rails/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:59+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/react/INDEX.md b/07-framework-security/frameworks/react/INDEX.md index 5318c4bf..86499cd1 100644 --- a/07-framework-security/frameworks/react/INDEX.md +++ b/07-framework-security/frameworks/react/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:56+00:00` +- 最近渲染时间: `2026-03-17T08:20:15+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/spring-boot/INDEX.md b/07-framework-security/frameworks/spring-boot/INDEX.md index 75fb2b64..abdbb550 100644 --- a/07-framework-security/frameworks/spring-boot/INDEX.md +++ b/07-framework-security/frameworks/spring-boot/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:59+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/spring-framework/INDEX.md b/07-framework-security/frameworks/spring-framework/INDEX.md index 2d7e1c79..0fbc2870 100644 --- a/07-framework-security/frameworks/spring-framework/INDEX.md +++ b/07-framework-security/frameworks/spring-framework/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:59+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/spring-security/INDEX.md b/07-framework-security/frameworks/spring-security/INDEX.md index d74516df..435c3405 100644 --- a/07-framework-security/frameworks/spring-security/INDEX.md +++ b/07-framework-security/frameworks/spring-security/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:59+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/sveltekit/INDEX.md b/07-framework-security/frameworks/sveltekit/INDEX.md index 3f5ea9f8..3c63dd66 100644 --- a/07-framework-security/frameworks/sveltekit/INDEX.md +++ b/07-framework-security/frameworks/sveltekit/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:58+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/symfony/INDEX.md b/07-framework-security/frameworks/symfony/INDEX.md index 25b37929..350a5461 100644 --- a/07-framework-security/frameworks/symfony/INDEX.md +++ b/07-framework-security/frameworks/symfony/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:59+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/undici/INDEX.md b/07-framework-security/frameworks/undici/INDEX.md index 585f95c1..9e7b5b61 100644 --- a/07-framework-security/frameworks/undici/INDEX.md +++ b/07-framework-security/frameworks/undici/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `14` -- 最近渲染时间: `2026-03-17T07:35:59+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/vite/INDEX.md b/07-framework-security/frameworks/vite/INDEX.md index d646d968..b47a666e 100644 --- a/07-framework-security/frameworks/vite/INDEX.md +++ b/07-framework-security/frameworks/vite/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `12` -- 最近渲染时间: `2026-03-17T07:35:58+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/vue/INDEX.md b/07-framework-security/frameworks/vue/INDEX.md index de7a2fc3..83638d78 100644 --- a/07-framework-security/frameworks/vue/INDEX.md +++ b/07-framework-security/frameworks/vue/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:57+00:00` +- 最近渲染时间: `2026-03-17T08:20:16+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/webpack/INDEX.md b/07-framework-security/frameworks/webpack/INDEX.md index b96f9d27..d7f2a562 100644 --- a/07-framework-security/frameworks/webpack/INDEX.md +++ b/07-framework-security/frameworks/webpack/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:59+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/frameworks/werkzeug/INDEX.md b/07-framework-security/frameworks/werkzeug/INDEX.md index 6418c943..26587f7f 100644 --- a/07-framework-security/frameworks/werkzeug/INDEX.md +++ b/07-framework-security/frameworks/werkzeug/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:59+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/platforms/adminer/INDEX.md b/07-framework-security/platforms/adminer/INDEX.md index 8b5c44f8..e8bcd6a9 100644 --- a/07-framework-security/platforms/adminer/INDEX.md +++ b/07-framework-security/platforms/adminer/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:59+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/platforms/gitea/INDEX.md b/07-framework-security/platforms/gitea/INDEX.md index 13622319..d8d99cc6 100644 --- a/07-framework-security/platforms/gitea/INDEX.md +++ b/07-framework-security/platforms/gitea/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `1` - 待人工/缺浏览器证据: `36` -- 最近渲染时间: `2026-03-17T07:36:01+00:00` +- 最近渲染时间: `2026-03-17T08:20:20+00:00` ## 目标约束 diff --git a/07-framework-security/platforms/gitlab-ce/INDEX.md b/07-framework-security/platforms/gitlab-ce/INDEX.md index db11d20b..77828aeb 100644 --- a/07-framework-security/platforms/gitlab-ce/INDEX.md +++ b/07-framework-security/platforms/gitlab-ce/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:36:01+00:00` +- 最近渲染时间: `2026-03-17T08:20:20+00:00` ## 目标约束 diff --git a/07-framework-security/platforms/grafana/INDEX.md b/07-framework-security/platforms/grafana/INDEX.md index 16e9e90f..4ee67b4d 100644 --- a/07-framework-security/platforms/grafana/INDEX.md +++ b/07-framework-security/platforms/grafana/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:36:01+00:00` +- 最近渲染时间: `2026-03-17T08:20:20+00:00` ## 目标约束 diff --git a/07-framework-security/platforms/jenkins/INDEX.md b/07-framework-security/platforms/jenkins/INDEX.md index 95c7955c..11e0876d 100644 --- a/07-framework-security/platforms/jenkins/INDEX.md +++ b/07-framework-security/platforms/jenkins/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:36:01+00:00` +- 最近渲染时间: `2026-03-17T08:20:20+00:00` ## 目标约束 diff --git a/07-framework-security/platforms/kibana/INDEX.md b/07-framework-security/platforms/kibana/INDEX.md index 8c1afc03..8b010041 100644 --- a/07-framework-security/platforms/kibana/INDEX.md +++ b/07-framework-security/platforms/kibana/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:36:01+00:00` +- 最近渲染时间: `2026-03-17T08:20:20+00:00` ## 目标约束 diff --git a/07-framework-security/platforms/mattermost/INDEX.md b/07-framework-security/platforms/mattermost/INDEX.md index 5c59da45..ff6c49e4 100644 --- a/07-framework-security/platforms/mattermost/INDEX.md +++ b/07-framework-security/platforms/mattermost/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:36:01+00:00` +- 最近渲染时间: `2026-03-17T08:20:20+00:00` ## 目标约束 diff --git a/07-framework-security/platforms/phpmyadmin/INDEX.md b/07-framework-security/platforms/phpmyadmin/INDEX.md index 981a9c86..c917599e 100644 --- a/07-framework-security/platforms/phpmyadmin/INDEX.md +++ b/07-framework-security/platforms/phpmyadmin/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:59+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/platforms/redmine/INDEX.md b/07-framework-security/platforms/redmine/INDEX.md index 6530ea33..95793c9e 100644 --- a/07-framework-security/platforms/redmine/INDEX.md +++ b/07-framework-security/platforms/redmine/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:36:01+00:00` +- 最近渲染时间: `2026-03-17T08:20:20+00:00` ## 目标约束 diff --git a/07-framework-security/servers/apache-httpd/INDEX.md b/07-framework-security/servers/apache-httpd/INDEX.md index 433eee24..6401ae3e 100644 --- a/07-framework-security/servers/apache-httpd/INDEX.md +++ b/07-framework-security/servers/apache-httpd/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:59+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/servers/apache-tomcat/INDEX.md b/07-framework-security/servers/apache-tomcat/INDEX.md index 4ef48c71..2a271812 100644 --- a/07-framework-security/servers/apache-tomcat/INDEX.md +++ b/07-framework-security/servers/apache-tomcat/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:59+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/servers/caddy/INDEX.md b/07-framework-security/servers/caddy/INDEX.md index 781b6d53..1f9e4a1d 100644 --- a/07-framework-security/servers/caddy/INDEX.md +++ b/07-framework-security/servers/caddy/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:59+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/servers/haproxy/INDEX.md b/07-framework-security/servers/haproxy/INDEX.md index f0a19002..d51e8885 100644 --- a/07-framework-security/servers/haproxy/INDEX.md +++ b/07-framework-security/servers/haproxy/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:59+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/servers/nginx/INDEX.md b/07-framework-security/servers/nginx/INDEX.md index 3c8e2570..788c5578 100644 --- a/07-framework-security/servers/nginx/INDEX.md +++ b/07-framework-security/servers/nginx/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:59+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/07-framework-security/servers/traefik/INDEX.md b/07-framework-security/servers/traefik/INDEX.md index 0564dcb8..f14d95c3 100644 --- a/07-framework-security/servers/traefik/INDEX.md +++ b/07-framework-security/servers/traefik/INDEX.md @@ -12,7 +12,7 @@ - 已实证(synthetic): `0` - 阻塞数: `0` - 待人工/缺浏览器证据: `0` -- 最近渲染时间: `2026-03-17T07:35:59+00:00` +- 最近渲染时间: `2026-03-17T08:20:17+00:00` ## 目标约束 diff --git a/08-threat-intel/generated/dashboard/assets/app.js b/08-threat-intel/generated/dashboard/assets/app.js index 7710b047..519437b9 100644 --- a/08-threat-intel/generated/dashboard/assets/app.js +++ b/08-threat-intel/generated/dashboard/assets/app.js @@ -1,4 +1,3 @@ - const state = { summary: null, runs: [], @@ -7,24 +6,42 @@ const state = { profiles: {}, selectedRunId: null, selectedArtifact: null, - filters: { search: "", system: "", status: "", family: "" }, - autoRefresh: true, - refreshMs: 5000, refreshHandle: null, + refreshMs: 5000, + autoRefresh: true, + filters: { + search: "", + system: "", + status: "", + profile: "" + }, + panels: { + timeline: true, + reasoning: true, + evidence: true, + logs: true, + sources: true, + run_json: false, + advisory_json: false, + profile_json: false + } }; const $ = (id) => document.getElementById(id); -const statusClass = (status) => `status-pill ${({ - "blocked-artifact": "status-blocked-artifact", - "blocked-destructive": "status-blocked-destructive", - "triage-manual": "status-triage-manual", - "verified-real": "status-verified-real", - "verified-synthetic": "status-verified-synthetic", - "suspected": "status-suspected", - "completed": "status-verified-real", - "failed": "status-blocked-artifact", - "skipped": "status-triage-manual" -})[status] || "status-default"}`; +const icon = (name, className = "icon") => + ``; + +const statusClass = (status) => ({ + "verified-real": "status-pill status-verified-real", + "verified-synthetic": "status-pill status-verified-synthetic", + "blocked-artifact": "status-pill status-blocked-artifact", + "blocked-destructive": "status-pill status-blocked-destructive", + "triage-manual": "status-pill status-triage-manual", + "suspected": "status-pill status-suspected", + completed: "status-pill status-verified-real", + failed: "status-pill status-blocked-artifact", + skipped: "status-pill status-triage-manual" +}[status] || "status-pill status-default"); function escapeHtml(value) { return String(value ?? "") @@ -34,6 +51,10 @@ function escapeHtml(value) { .replaceAll('"', """); } +function formatStatus(value) { + return String(value || "unknown").replaceAll("-", " "); +} + function timeAgo(value) { if (!value) return "-"; const diff = Date.now() - new Date(value).getTime(); @@ -56,38 +77,93 @@ async function fetchJson(url) { return response.json(); } -async function loadData(preserveSelection = true) { - $("syncState").innerHTML = `Refreshing${new Date().toLocaleTimeString()}`; - const previousRun = state.selectedRunId; - try { - const [summary, runs, systems, advisories, profiles] = await Promise.all([ - fetchJson("./summary.json"), - fetchJson("./runs.json"), - fetchJson("./systems.json"), - fetchJson("./advisories.json"), - fetchJson("./profiles.json"), - ]); - state.summary = summary; - state.runs = runs; - state.systems = systems; - state.advisories = advisories; - state.profiles = profiles; - hydrateFilterOptions(); +function distinct(values) { + return [...new Set(values.filter(Boolean))].sort(); +} - const hashRun = location.hash.startsWith("#run=") ? location.hash.replace("#run=", "") : null; - const selectedCandidate = preserveSelection ? (hashRun || previousRun) : hashRun; - if (selectedCandidate && runs.some((item) => item.run_id === selectedCandidate)) { - state.selectedRunId = selectedCandidate; - } else { - state.selectedRunId = runs[0]?.run_id || null; +function sumStatuses(predicate) { + return Object.entries(state.summary?.statuses || {}) + .filter(([key]) => predicate(key)) + .reduce((sum, [, value]) => sum + Number(value || 0), 0); +} + +function metricCards() { + const successCount = Number(state.summary?.statuses?.["verified-real"] || 0) + Number(state.summary?.statuses?.["verified-synthetic"] || 0); + const blockedCount = sumStatuses((key) => key.startsWith("blocked")); + const inProgressCount = Math.max(Number(state.summary?.run_count || 0) - successCount - blockedCount, 0); + + return [ + { + label: "Total Runs", + value: state.summary?.run_count || 0, + note: `${state.summary?.advisory_count || 0} advisories indexed`, + color: "var(--accent-purple)", + iconName: "report" + }, + { + label: "Success", + value: successCount, + note: "verified-real + verified-synthetic", + color: "var(--accent-green)", + iconName: "shield" + }, + { + label: "Blocked", + value: blockedCount, + note: "artifact or destructive blockers", + color: "var(--accent-red)", + iconName: "failure" + }, + { + label: "In Progress", + value: inProgressCount, + note: "manual review or incomplete verification", + color: "var(--accent-blue)", + iconName: "timeline" } + ]; +} - renderDashboard(); - $("syncState").innerHTML = `Live${summary.generated_at || new Date().toISOString()}`; - } catch (error) { - $("syncState").innerHTML = `Load Failed${escapeHtml(error.message)}`; - $("runList").innerHTML = `
Dashboard load failed: ${escapeHtml(error.message)}
`; - $("detailRoot").innerHTML = `
Unable to load dashboard data. Check generated JSON and local static server state.
`; +function renderMetrics() { + $("metricCards").innerHTML = metricCards() + .map( + (card) => ` +
+
${icon(card.iconName)}${escapeHtml(card.label)}
+
${escapeHtml(card.value)}
+
${escapeHtml(card.note)}
+
+ ` + ) + .join(""); +} + +function renderSyncState(kind, title, detail) { + $("syncState").innerHTML = ` + ${icon("sync", "icon icon-sync")} +
+ ${escapeHtml(title)} + ${escapeHtml(detail)} +
+ `; + $("syncState").dataset.kind = kind; +} + +function hydrateFilters() { + const controls = [ + ["systemFilter", "system", state.runs.map((item) => item.system_id), "All systems"], + ["statusFilter", "status", state.runs.map((item) => item.verification_status), "All statuses"], + ["profileFilter", "profile", state.runs.map((item) => item.repro_profile_id), "All profiles"] + ]; + + for (const [id, key, values, label] of controls) { + const control = $(id); + const current = state.filters[key]; + control.innerHTML = ``; + control.innerHTML += distinct(values) + .map((value) => ``) + .join(""); + control.value = current; } } @@ -95,95 +171,95 @@ function filteredRuns() { return state.runs.filter((item) => { if (state.filters.system && item.system_id !== state.filters.system) return false; if (state.filters.status && item.verification_status !== state.filters.status) return false; - if (state.filters.family && item.repro_profile_id !== state.filters.family) return false; + if (state.filters.profile && item.repro_profile_id !== state.filters.profile) return false; if (!state.filters.search) return true; - const advisoryTitle = item.advisory_meta?.title || ""; - const haystack = [item.run_id, item.advisory_id, item.system_id, item.repro_profile_id, advisoryTitle] + const haystack = [ + item.run_id, + item.advisory_id, + item.system_id, + item.repro_profile_id, + item.advisory_meta?.title || "", + item.advisory_meta?.summary || "" + ] .join(" ") .toLowerCase(); return haystack.includes(state.filters.search); }); } -function renderMetrics() { - const metrics = [ - { label: "Advisories", value: state.summary?.advisory_count ?? 0 }, - { label: "Run Bundles", value: state.summary?.run_count ?? 0 }, - ...Object.entries(state.summary?.statuses || {}).map(([label, value]) => ({ label, value })), - ]; - $("metrics").innerHTML = metrics - .map((item) => `
${escapeHtml(item.label)}${escapeHtml(item.value)}
`) - .join(""); +function renderSystems() { + $("systemStats").innerHTML = state.systems.length + ? state.systems + .map((system) => { + const total = Math.max(Number(system.total || 0), 1); + const verified = Number(system.verified_real || 0) + Number(system.verified_synthetic || 0); + const coverage = Math.round((verified / total) * 100); + return ` +
+
+ ${escapeHtml(system.display_name || system.system_id)} + ${escapeHtml(system.browser_present || 0)}/${escapeHtml(system.browser_required || 0)} browser +
+
${escapeHtml(system.system_id)} · latest ${escapeHtml(system.latest_update || "-")}
+
+ real ${escapeHtml(system.verified_real || 0)} + synthetic ${escapeHtml(system.verified_synthetic || 0)} + blocked ${escapeHtml(system.blocked || 0)} +
+
+
+ `; + }) + .join("") + : `
No system coverage data.
`; } -function renderSystemCoverage() { - $("systemCoverage").innerHTML = state.systems - .map((system) => { - const total = Math.max(system.total || 0, 1); - const verified = (system.verified_real || 0) + (system.verified_synthetic || 0); - const fill = Math.round((verified / total) * 100); - return ` -
-
- ${escapeHtml(system.display_name || system.system_id)} - ${escapeHtml(system.browser_present || 0)}/${escapeHtml(system.browser_required || 0)} browser -
-
${escapeHtml(system.system_id)} · latest ${escapeHtml(system.latest_update || "-")}
-
- real ${escapeHtml(system.verified_real || 0)} - synthetic ${escapeHtml(system.verified_synthetic || 0)} - blocked ${escapeHtml(system.blocked || 0)} - manual ${escapeHtml(system.manual || 0)} -
-
-
- `; - }) - .join(""); -} - -function renderFailures() { +function renderRecentFailures() { const failures = state.summary?.recent_failures || []; - $("failureFeed").innerHTML = failures.length + $("recentFailures").innerHTML = failures.length ? failures - .map((item) => ` -
-
- ${escapeHtml(item.run_id)} - ${escapeHtml(item.status)} -
-
${escapeHtml(item.title || item.advisory_id)}
-
${escapeHtml(item.blocked_reason || "-")}
-
- `) + .map( + (item) => ` +
+
+ ${escapeHtml(item.run_id)} + ${escapeHtml(formatStatus(item.status))} +
+
${escapeHtml(item.title || item.advisory_id)}
+
${escapeHtml(item.blocked_reason || "-")}
+
+ ` + ) .join("") : `
No recent blockers.
`; } -function renderRunList() { - const filtered = filteredRuns(); - $("runCount").textContent = `${filtered.length} shown`; - $("runList").innerHTML = filtered.length - ? filtered +function renderRunQueue() { + const runs = filteredRuns(); + $("runCount").textContent = `${runs.length} shown`; + $("runQueue").innerHTML = runs.length + ? runs .map((item) => { const active = item.run_id === state.selectedRunId ? "is-active" : ""; - const title = item.advisory_meta?.title || item.advisory_id; - const reasoning = item.reasoning_lines?.[0] || item.blocked_reason || ""; - const browserLabel = item.browser_evidence?.present ? "ready" : (item.browser_evidence?.required ? "required" : "n/a"); + const browserState = item.browser_evidence?.present ? "ready" : (item.browser_evidence?.required ? "required" : "optional"); + const lead = item.reasoning_lines?.[0] || item.blocked_reason || item.advisory_meta?.summary || ""; return ` - `; }) @@ -194,44 +270,65 @@ function renderRunList() { button.addEventListener("click", () => { state.selectedRunId = button.dataset.runId; location.hash = `run=${state.selectedRunId}`; - renderRunList(); + renderRunQueue(); renderDetail(); }); }); } -function renderDashboard() { - renderMetrics(); - renderSystemCoverage(); - renderFailures(); - renderRunList(); - renderDetail(); +function progressSegments(progress) { + const order = [ + ["completed", "Completed", "progress-completed"], + ["blocked", "Blocked", "progress-blocked"], + ["failed", "Failed", "progress-failed"], + ["skipped", "Skipped", "progress-skipped"], + ["planned", "Planned", "progress-planned"], + ["other", "Other", "progress-other"] + ]; + const total = order.reduce((sum, [key]) => sum + Number(progress?.[key] || 0), 0); + if (!total) { + return { + bar: `
`, + legend: `no progress` + }; + } + const bar = order + .filter(([key]) => Number(progress?.[key] || 0) > 0) + .map(([key, _label, className]) => { + const pct = Math.max((Number(progress[key] || 0) / total) * 100, 4); + return ``; + }) + .join(""); + const legend = order + .filter(([key]) => Number(progress?.[key] || 0) > 0) + .map(([key, label, className]) => `${escapeHtml(label)} ${escapeHtml(progress[key] || 0)}`) + .join(""); + return { bar, legend }; } -function setFilterListeners() { - [["searchInput", "search"], ["systemFilter", "system"], ["statusFilter", "status"], ["familyFilter", "family"]].forEach(([id, key]) => { - $(id).addEventListener("input", (event) => { - state.filters[key] = String(event.target.value || "").trim().toLowerCase(); - if (key !== "search") { - state.filters[key] = String(event.target.value || ""); - } - renderRunList(); - }); - }); +function timelineTone(status) { + if (status === "completed" || status === "verified-real" || status === "verified-synthetic") return "timeline-success"; + if (String(status || "").startsWith("blocked") || status === "failed") return "timeline-blocked"; + if (status === "planned") return "timeline-pending"; + return "timeline-neutral"; } -function hydrateFilterOptions() { - const distinct = (items) => [...new Set(items.filter(Boolean))].sort(); - const patchOptions = (id, values) => { - const control = $(id); - const current = control.value; - control.innerHTML = control.dataset.base; - control.innerHTML += distinct(values).map((value) => ``).join(""); - control.value = current; - }; - patchOptions("systemFilter", state.runs.map((item) => item.system_id)); - patchOptions("statusFilter", state.runs.map((item) => item.verification_status)); - patchOptions("familyFilter", state.runs.map((item) => item.repro_profile_id)); +function renderPanel(panelKey, title, meta, iconName, content) { + const open = state.panels[panelKey] !== false; + return ` +
+ +
+
${content}
+
+
+ `; } function defaultArtifact(run) { @@ -239,94 +336,35 @@ function defaultArtifact(run) { for (const key of preference) { const group = (run.artifact_groups || []).find((item) => item.key === key && item.items?.length); if (!group) continue; - const preferredText = group.items.find((item) => item.kind === "text"); - return preferredText || group.items[0]; + const textItem = group.items.find((item) => item.kind === "text"); + return textItem || group.items[0]; } return null; } -function totalProgress(progress) { - const values = Object.values(progress || {}).map((value) => Number(value || 0)); - return values.reduce((sum, value) => sum + value, 0); -} - -function renderProgressStrip(progress) { - const total = totalProgress(progress); - if (!total) { - return ` -
-
-
No timeline progress recorded.
-
- `; - } - const order = [ - ["completed", "Completed", "progress-completed"], - ["blocked", "Blocked", "progress-blocked"], - ["failed", "Failed", "progress-failed"], - ["skipped", "Skipped", "progress-skipped"], - ["planned", "Planned", "progress-planned"], - ["other", "Other", "progress-other"], - ]; - const segments = order - .filter(([key]) => Number(progress?.[key] || 0) > 0) - .map(([key, _label, klass]) => { - const count = Number(progress?.[key] || 0); - const pct = Math.max((count / total) * 100, 4); - return `
`; - }) - .join(""); - const legend = order - .filter(([key]) => Number(progress?.[key] || 0) > 0) - .map(([key, label, klass]) => ` - - - ${escapeHtml(label)} ${escapeHtml(progress?.[key] || 0)} - - `) - .join(""); - return ` -
-
${segments}
-
${legend}
-
- `; -} - -function renderStageCards(run) { - const timeline = run.timeline || []; - if (!timeline.length) { - return `
No stage records available.
`; - } - return ` -
- ${timeline.map((item) => ` -
- ${escapeHtml(item.step || "-")} -
${escapeHtml(item.status || "unknown")}
-
${escapeHtml(item.detail || "-")}
-
${escapeHtml(item.at || "-")}
-
- `).join("")} -
- `; -} - async function openArtifact(href, label, kind) { state.selectedArtifact = { href, label, kind }; document.querySelectorAll(".artifact-button").forEach((button) => { button.classList.toggle("is-active", button.dataset.href === href); }); - $("artifactLabel").textContent = label; - $("artifactOpen").href = href; - $("artifactMeta").textContent = href; + + const labelNode = $("viewerLabel"); + const metaNode = $("viewerMeta"); + const openNode = $("viewerOpen"); + const viewer = $("viewerFrame"); + if (!labelNode || !metaNode || !openNode || !viewer) return; + + labelNode.textContent = label; + metaNode.textContent = href; + openNode.href = href; + try { if (kind === "image") { - $("artifactViewer").innerHTML = `${escapeHtml(label)}`; + viewer.innerHTML = `${escapeHtml(label)}`; return; } if (href.endsWith(".html")) { - $("artifactViewer").innerHTML = ``; + viewer.innerHTML = ``; return; } const response = await fetch(`${href}?t=${Date.now()}`, { cache: "no-store" }); @@ -339,211 +377,274 @@ async function openArtifact(href, label, kind) { } catch (_error) { } } - $("artifactViewer").innerHTML = `
${escapeHtml(formatted)}
`; + viewer.innerHTML = `
${escapeHtml(formatted)}
`; } catch (error) { - $("artifactViewer").innerHTML = `
Artifact load failed: ${escapeHtml(error.message)}
`; + viewer.innerHTML = `
Artifact load failed: ${escapeHtml(error.message)}
`; } } +function bindPanelToggles() { + document.querySelectorAll("[data-panel-toggle]").forEach((button) => { + button.addEventListener("click", () => { + const key = button.dataset.panelToggle; + state.panels[key] = !(state.panels[key] !== false); + const panel = document.querySelector(`[data-panel="${key}"]`); + if (panel) { + panel.classList.toggle("is-collapsed", state.panels[key] === false); + } + }); + }); +} + function renderDetail() { const run = state.runs.find((item) => item.run_id === state.selectedRunId); if (!run) { - $("detailRoot").innerHTML = `
Select a run to inspect full timeline, logs, sources, and reasoning.
`; + $("detailWorkspace").innerHTML = ` +
+ ${icon("shield", "icon icon-xl")} +

Select a run

+

Pick a run from the left queue to inspect timeline, evidence, logs and raw JSON.

+
+ `; return; } const advisory = run.advisory_meta || {}; const profile = run.profile_meta || {}; - const screenshotItems = (run.artifact_groups || []) - .find((group) => group.key === "browser") - ?.items.filter((item) => item.kind === "image") || []; + const screenshotItems = ((run.artifact_groups || []).find((group) => group.key === "browser")?.items || []).filter((item) => item.kind === "image"); + const segments = progressSegments(run.progress || {}); + const browserStatus = run.browser_evidence?.present ? "Ready" : (run.browser_evidence?.required ? "Required" : "Optional"); + const artifactCount = (run.artifact_groups || []).reduce((sum, group) => sum + Number(group.count || 0), 0); - $("detailRoot").innerHTML = ` -
-
Local Verification Workspace
-
- ${escapeHtml(run.verification_status)} -
- ${escapeHtml(run.system_id)} - ${escapeHtml(run.repro_profile_id)} - ${escapeHtml(run.artifact_mode)} - ${escapeHtml(run.verification_mode)} - ${escapeHtml(run.target_env || "local-docker")} -
-
-

${escapeHtml(advisory.title || run.advisory_id)}

-

${escapeHtml(advisory.summary || "No summary available.")}

- -
-
Timeline Steps${escapeHtml(run.timeline?.length || 0)}
-
Artifacts${escapeHtml((run.artifact_groups || []).reduce((sum, group) => sum + group.count, 0))}
-
Browser${run.browser_evidence?.present ? "Ready" : (run.browser_evidence?.required ? "Required" : "Optional")}
-
Finished${escapeHtml(timeAgo(run.finished_at))}
-
-
- -
-
-
- Progress Timeline${escapeHtml(run.timeline?.length || 0)} steps -
- ${renderProgressStrip(run.progress)} - ${renderStageCards(run)} -
- ${(run.timeline || []).map((item) => ` -
-
${escapeHtml(item.at || "-")}
-
${escapeHtml(item.step || "-")}
-
-
${escapeHtml(item.status || "unknown")}
-
${escapeHtml(item.detail || "-")}
-
-
- `).join("") || `
No timeline items available.
`} + const timelineContent = ` +
${segments.bar}
+
${segments.legend}
+
+ ${(run.timeline || []) + .map((item) => ` +
+ +
+ ${escapeHtml(item.step || "-")} + ${escapeHtml(item.at || "-")}
-
-
- -
- Attack Plan & Reasoning${escapeHtml(profile.vuln_family || "unknown")} -
- ${run.blocked_reason ? `
Failure reason
${escapeHtml(run.blocked_reason)}
` : ""} -
- destructive risk ${escapeHtml(profile.destructive_risk || "-")} - cleanup ${escapeHtml(profile.cleanup_policy || "-")} - targets ${(profile.allowed_target_types || []).join(", ") || "-"} -
-
- ${(run.reasoning_lines || []).map((line) => `
${escapeHtml(line)}
`).join("")} -
-
- ${(profile.success_criteria || []).map((line) => `${escapeHtml(line)}`).join("")} -
-
-
- -
- Evidence Explorer${escapeHtml((run.artifact_groups || []).length)} groups -
- ${(run.artifact_groups || []).map((group) => ` -
-

${escapeHtml(group.label)} · ${escapeHtml(group.count)}

-
- ${group.items.map((item) => ` - - `).join("")} -
-
- `).join("") || `
No artifacts linked for this run.
`} - ${screenshotItems.length ? ` - - ` : ""} -
-
- -
- Live Log Viewer${state.selectedArtifact ? "active" : "idle"} -
-
-
-
- ${escapeHtml(state.selectedArtifact?.label || "Select an artifact")} -
${escapeHtml(state.selectedArtifact?.href || "Artifacts and logs can be previewed here.")}
-
-
- Open artifact - -
-
-
Select a report, log, JSON, screenshot, or timeline file to preview it here.
-
-
-
-
- -
-
- Sources & Fix Topics${escapeHtml((advisory.secondary_source_urls || []).length + (advisory.official_source_url ? 1 : 0))} links -
-
- ${(advisory.aliases || []).map((alias) => `${escapeHtml(alias)}`).join("")} -
-
- ${advisory.official_source_url ? `${escapeHtml(advisory.official_source_url)}` : `
No official source linked.
`} - ${(advisory.secondary_source_urls || []).map((ref) => `${escapeHtml(ref)}`).join("")} -
-
- ${(advisory.secure_code_topics || []).map((topic) => `${escapeHtml(topic)}`).join("")} -
-
-
- -
- Run JSONraw -
${escapeHtml(JSON.stringify(run, null, 2))}
-
- -
- Advisory JSONraw -
${escapeHtml(JSON.stringify(advisory, null, 2))}
-
- -
- Profile JSONraw -
${escapeHtml(JSON.stringify(profile, null, 2))}
-
-
+
${escapeHtml(formatStatus(item.status || "unknown"))}
+
${escapeHtml(item.detail || "-")}
+ + `) + .join("") || `
No timeline items recorded.
`}
`; - document.querySelectorAll(".artifact-button").forEach((button) => { + const reasoningCards = [ + { + label: "Summary", + copy: advisory.summary || "No advisory summary available." + }, + { + label: "Success Criteria", + copy: (profile.success_criteria || []).join(" | ") || "No success criteria defined." + }, + { + label: "Seed / Attack Notes", + copy: (run.reasoning_lines || []).join("\n\n") || "No reasoning lines recorded." + }, + { + label: "Allowed Targets", + copy: (profile.allowed_target_types || []).join(", ") || "No target scope declared." + } + ]; + + const reasoningContent = ` + ${run.blocked_reason ? `
Failure reason
${escapeHtml(run.blocked_reason)}
` : ""} +
+ vuln family ${escapeHtml(profile.vuln_family || "unknown")} + cleanup ${escapeHtml(profile.cleanup_policy || "-")} + destructive risk ${escapeHtml(profile.destructive_risk || "-")} + artifact ${escapeHtml(run.artifact_mode || "-")} +
+
+ ${reasoningCards + .map( + (card) => ` +
+ ${escapeHtml(card.label)} +
${escapeHtml(card.copy)}
+
+ ` + ) + .join("")} +
+ `; + + const evidenceContent = ` +
+ ${(run.artifact_groups || []) + .map( + (group) => ` +
+

${escapeHtml(group.label)} · ${escapeHtml(group.count)}

+
+ ${group.items + .map( + (item) => ` + + ` + ) + .join("")} +
+
+ ` + ) + .join("") || `
No artifact groups for this run.
`} + + ${ + screenshotItems.length + ? `` + : "" + } +
+ `; + + const logContent = ` +
+
+
+
${escapeHtml(state.selectedArtifact?.label || "Select an artifact")}
+
${escapeHtml(state.selectedArtifact?.href || "Artifact preview will appear here.")}
+
+
+ ${icon("link")}Open artifact + +
+
+
Select a report, log, screenshot, JSON or HTML artifact to preview it here.
+
+ `; + + const sourcesContent = ` +
+ ${(advisory.aliases || []).map((alias) => `${escapeHtml(alias)}`).join("")} + ${(advisory.secure_code_topics || []).map((topic) => `${escapeHtml(topic)}`).join("")} +
+ + `; + + const rawRunContent = `
${escapeHtml(JSON.stringify(run, null, 2))}
`; + const rawAdvisoryContent = `
${escapeHtml(JSON.stringify(advisory, null, 2))}
`; + const rawProfileContent = `
${escapeHtml(JSON.stringify(profile, null, 2))}
`; + + $("detailWorkspace").innerHTML = ` +
+
+ ${escapeHtml(formatStatus(run.verification_status))} +
+ ${escapeHtml(run.system_id)} + ${escapeHtml(run.repro_profile_id)} + ${escapeHtml(run.verification_mode || "-")} + ${escapeHtml(run.target_env || "-")} +
+
+

${escapeHtml(advisory.title || run.advisory_id)}

+
${escapeHtml(advisory.summary || "No advisory summary available.")}
+ + + +
+
+ Timeline Steps + ${escapeHtml(run.timeline?.length || 0)} +
+
+ Artifacts + ${escapeHtml(artifactCount)} +
+
+ Browser Evidence + ${escapeHtml(browserStatus)} +
+
+ Finished + ${escapeHtml(timeAgo(run.finished_at))} +
+
+
+ + ${renderPanel("timeline", "Progress Timeline", `${escapeHtml(run.timeline?.length || 0)} steps`, "timeline", timelineContent)} + ${renderPanel("reasoning", "Attack Plan & Reasoning", escapeHtml(profile.vuln_family || "unknown"), "reasoning", reasoningContent)} + ${renderPanel("evidence", "Evidence Explorer", `${escapeHtml(run.artifact_groups?.length || 0)} groups`, "evidence", evidenceContent)} + ${renderPanel("logs", "Live Log Viewer", state.selectedArtifact ? "active" : "idle", "logs", logContent)} + ${renderPanel("sources", "Sources & Fix Topics", `${escapeHtml((advisory.secondary_source_urls || []).length + (advisory.official_source_url ? 1 : 0))} links`, "sources", sourcesContent)} + ${renderPanel("run_json", "Run JSON", "raw", "json", rawRunContent)} + ${renderPanel("advisory_json", "Advisory JSON", "raw", "json", rawAdvisoryContent)} + ${renderPanel("profile_json", "Profile JSON", "raw", "json", rawProfileContent)} + `; + + bindPanelToggles(); + + document.querySelectorAll("[data-artifact]").forEach((button) => { button.addEventListener("click", () => openArtifact(button.dataset.href, button.dataset.label, button.dataset.kind)); }); - $("refreshArtifact")?.addEventListener("click", () => { + $("viewerRefresh")?.addEventListener("click", () => { if (state.selectedArtifact) { openArtifact(state.selectedArtifact.href, state.selectedArtifact.label, state.selectedArtifact.kind); } }); - if (!state.selectedArtifact || !(run.artifact_groups || []).some((group) => group.items.some((item) => item.href === state.selectedArtifact.href))) { - const artifact = defaultArtifact(run); - if (artifact) { - openArtifact(artifact.href, artifact.label, artifact.kind); - } - } else { - openArtifact(state.selectedArtifact.href, state.selectedArtifact.label, state.selectedArtifact.kind); + const artifactExists = (run.artifact_groups || []).some((group) => group.items.some((item) => item.href === state.selectedArtifact?.href)); + const defaultItem = artifactExists ? state.selectedArtifact : defaultArtifact(run); + if (defaultItem) { + openArtifact(defaultItem.href, defaultItem.label, defaultItem.kind); } } -function attachGlobalActions() { +function renderAll() { + renderMetrics(); + renderSystems(); + renderRecentFailures(); + renderRunQueue(); + renderDetail(); +} + +function attachGlobalEvents() { $("searchInput").addEventListener("input", (event) => { state.filters.search = String(event.target.value || "").trim().toLowerCase(); - renderRunList(); + renderRunQueue(); }); - [["systemFilter", "system"], ["statusFilter", "status"], ["familyFilter", "family"]].forEach(([id, key]) => { + + [ + ["systemFilter", "system"], + ["statusFilter", "status"], + ["profileFilter", "profile"] + ].forEach(([id, key]) => { $(id).addEventListener("input", (event) => { state.filters[key] = String(event.target.value || ""); - renderRunList(); + renderRunQueue(); }); }); + $("refreshDashboard").addEventListener("click", () => loadData(false)); $("autoRefresh").addEventListener("change", (event) => { state.autoRefresh = Boolean(event.target.checked); @@ -560,11 +661,45 @@ function startRefreshLoop() { state.refreshHandle = setInterval(() => loadData(true), state.refreshMs); } +async function loadData(preserveSelection = true) { + const previous = state.selectedRunId; + renderSyncState("loading", "Refreshing", new Date().toLocaleTimeString()); + + try { + const [summary, runs, systems, advisories, profiles] = await Promise.all([ + fetchJson("./summary.json"), + fetchJson("./runs.json"), + fetchJson("./systems.json"), + fetchJson("./advisories.json"), + fetchJson("./profiles.json") + ]); + + state.summary = summary; + state.runs = runs; + state.systems = systems; + state.advisories = advisories; + state.profiles = profiles; + hydrateFilters(); + + const hashRun = location.hash.startsWith("#run=") ? location.hash.replace("#run=", "") : null; + const candidate = preserveSelection ? (hashRun || previous) : hashRun; + if (candidate && runs.some((item) => item.run_id === candidate)) { + state.selectedRunId = candidate; + } else { + state.selectedRunId = runs[0]?.run_id || null; + } + + renderAll(); + renderSyncState("live", "Live", summary.generated_at || new Date().toISOString()); + } catch (error) { + $("runQueue").innerHTML = `
Dashboard load failed: ${escapeHtml(error.message)}
`; + $("detailWorkspace").innerHTML = `

Load failed

${escapeHtml(error.message)}

`; + renderSyncState("error", "Load Failed", error.message); + } +} + async function init() { - ["systemFilter", "statusFilter", "familyFilter"].forEach((id) => { - $(id).dataset.base = $(id).innerHTML; - }); - attachGlobalActions(); + attachGlobalEvents(); await loadData(false); startRefreshLoop(); window.addEventListener("hashchange", () => loadData(false)); diff --git a/08-threat-intel/generated/dashboard/assets/design-source.json b/08-threat-intel/generated/dashboard/assets/design-source.json new file mode 100644 index 00000000..a46e5036 --- /dev/null +++ b/08-threat-intel/generated/dashboard/assets/design-source.json @@ -0,0 +1,18 @@ +{ + "template_id": "lovart-authorized-lab-dashboard", + "source_url": "https://assets-persist.lovart.ai/agent_images/464011bb-fbbc-4bd4-98f8-90897dd43612.html", + "downloaded_at": "2026-03-17T07:56:29Z", + "original_filename": "464011bb-fbbc-4bd4-98f8-90897dd43612.html", + "vendor_source_path": "scripts/lab/dashboard_templates/lovart/vendor/464011bb-fbbc-4bd4-98f8-90897dd43612.html", + "runtime_template": { + "index": "scripts/lab/dashboard_templates/lovart/index.html", + "styles": "scripts/lab/dashboard_templates/lovart/assets/styles.css", + "app": "scripts/lab/dashboard_templates/lovart/assets/app.js", + "icons": "scripts/lab/dashboard_templates/lovart/assets/icons.svg" + }, + "notes": [ + "The remote Lovart HTML is tracked for provenance only and is not used at runtime.", + "Runtime assets are localized into repository-managed templates and generated output.", + "External fonts and icon CDNs are intentionally removed from the generated dashboard." + ] +} diff --git a/08-threat-intel/generated/dashboard/assets/icons.svg b/08-threat-intel/generated/dashboard/assets/icons.svg new file mode 100644 index 00000000..714a6d98 --- /dev/null +++ b/08-threat-intel/generated/dashboard/assets/icons.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/08-threat-intel/generated/dashboard/assets/styles.css b/08-threat-intel/generated/dashboard/assets/styles.css index 61c46ff2..4788a321 100644 --- a/08-threat-intel/generated/dashboard/assets/styles.css +++ b/08-threat-intel/generated/dashboard/assets/styles.css @@ -1,280 +1,540 @@ - :root { - --bg: #07111f; - --panel: rgba(9, 18, 32, 0.86); - --panel-2: rgba(10, 24, 44, 0.92); - --panel-soft: rgba(18, 32, 56, 0.74); - --border: rgba(137, 171, 214, 0.22); - --text: #f7fafc; - --muted: #9fb3ca; - --accent: #5eead4; - --accent-2: #ffb86b; - --accent-3: #90cdf4; - --danger: #ff7b7b; - --warning: #ffd166; - --success: #6ee7a5; - --shadow: 0 24px 80px rgba(1, 7, 20, 0.45); - --radius: 20px; + --bg-dark: #0b1020; + --bg-deeper: #11182a; + --bg-card: rgba(21, 27, 40, 0.88); + --bg-card-strong: rgba(19, 25, 38, 0.96); + --bg-card-hover: rgba(34, 42, 60, 0.94); + --accent-blue: #4d8dff; + --accent-purple: #7c5cff; + --accent-green: #1ed49d; + --accent-red: #ff6b7a; + --accent-yellow: #ffb547; + --text-primary: #edf2ff; + --text-secondary: #95a2c2; + --text-dim: #7080a3; + --border-color: rgba(148, 163, 184, 0.18); + --border-strong: rgba(148, 163, 184, 0.3); + --shadow-lg: 0 24px 80px rgba(2, 6, 23, 0.45); + --shadow-md: 0 16px 40px rgba(2, 6, 23, 0.34); + --radius-xl: 24px; + --radius-lg: 18px; + --radius-md: 14px; + --radius-sm: 10px; } -* { box-sizing: border-box; } -html, body { margin: 0; min-height: 100%; } +* { + box-sizing: border-box; +} + +html, body { - font-family: "IBM Plex Sans", "Avenir Next", "Segoe UI", sans-serif; + margin: 0; + min-height: 100%; +} + +body { + font-family: "IBM Plex Sans", "Avenir Next", "Segoe UI", system-ui, sans-serif; + color: var(--text-primary); background: - radial-gradient(circle at top left, rgba(94, 234, 212, 0.15), transparent 28%), - radial-gradient(circle at top right, rgba(255, 184, 107, 0.18), transparent 22%), - linear-gradient(145deg, #050c16 0%, #08111f 44%, #0d1c31 100%); - color: var(--text); - overflow-x: hidden; + radial-gradient(circle at 12% 18%, rgba(77, 141, 255, 0.16), transparent 26%), + radial-gradient(circle at 86% 22%, rgba(124, 92, 255, 0.16), transparent 24%), + linear-gradient(180deg, #08111f 0%, #0a1323 46%, #0d1728 100%); } -body::before { - content: ""; - position: fixed; - inset: 0; - pointer-events: none; - background-image: - linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px), - linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px); - background-size: 32px 32px; - mask-image: radial-gradient(circle at center, black 36%, transparent 78%); - opacity: 0.28; +a { + color: inherit; + text-decoration: none; } -a { color: var(--accent); text-decoration: none; } -a:hover { text-decoration: underline; } -button, input, select { +button, +input, +select { font: inherit; } +.grid-bg { + position: fixed; + inset: 0; + background-image: + linear-gradient(rgba(148, 163, 184, 0.08) 1px, transparent 1px), + linear-gradient(90deg, rgba(148, 163, 184, 0.08) 1px, transparent 1px); + background-size: 38px 38px; + mask-image: radial-gradient(circle at center, black 28%, transparent 82%); + pointer-events: none; + z-index: -1; +} + .dashboard-shell { - position: relative; - max-width: 1640px; + max-width: 1760px; margin: 0 auto; - padding: 32px 24px 40px; + padding: 20px 20px 28px; +} + +.icon { + width: 16px; + height: 16px; + flex: 0 0 auto; +} + +.icon-xl { + width: 28px; + height: 28px; } .hero { position: sticky; top: 0; - z-index: 20; - backdrop-filter: blur(18px); - background: linear-gradient(180deg, rgba(7, 17, 31, 0.94), rgba(7, 17, 31, 0.75)); - border: 1px solid var(--border); + z-index: 50; + padding: 24px 24px 22px; + border: 1px solid var(--border-color); border-radius: 28px; - padding: 24px 24px 20px; - box-shadow: var(--shadow); + background: + linear-gradient(135deg, rgba(11, 16, 32, 0.96) 0%, rgba(17, 24, 42, 0.94) 48%, rgba(27, 18, 52, 0.96) 100%); + backdrop-filter: blur(18px); + box-shadow: var(--shadow-lg); + overflow: hidden; } -.hero-grid { +.hero-glow { + position: absolute; + width: 420px; + height: 420px; + border-radius: 999px; + filter: blur(90px); + pointer-events: none; +} + +.hero-glow-left { + top: -180px; + left: -120px; + background: rgba(77, 141, 255, 0.22); +} + +.hero-glow-right { + top: -220px; + right: -100px; + background: rgba(124, 92, 255, 0.2); +} + +.hero-top { + position: relative; display: grid; - grid-template-columns: 1.6fr 1fr; - gap: 20px; + grid-template-columns: minmax(0, 1.25fr) minmax(360px, 0.95fr); + gap: 24px; align-items: start; } -.eyebrow { +.hero-eyebrow { display: inline-flex; align-items: center; gap: 8px; - color: var(--muted); - font-size: 0.88rem; - letter-spacing: 0.12em; + color: var(--text-secondary); text-transform: uppercase; + letter-spacing: 0.14em; + font-size: 0.78rem; } -.eyebrow::before { - content: ""; - width: 10px; - height: 10px; - border-radius: 999px; - background: radial-gradient(circle, var(--accent), rgba(94, 234, 212, 0.15)); - box-shadow: 0 0 24px rgba(94, 234, 212, 0.8); - animation: pulse 2.8s ease-in-out infinite; -} - -.hero h1 { - margin: 12px 0 10px; - font-family: "IBM Plex Serif", "Iowan Old Style", Georgia, serif; +.hero-copy h1 { + margin: 14px 0 12px; + font-family: "IBM Plex Serif", Georgia, serif; font-size: clamp(2rem, 4vw, 3.5rem); line-height: 1.02; } -.hero p { +.hero-copy p { margin: 0; - color: var(--muted); - max-width: 74ch; + max-width: 72ch; + color: var(--text-secondary); + line-height: 1.6; } .hero-actions { - display: flex; - flex-wrap: wrap; - gap: 12px; - margin-top: 18px; + position: relative; + display: grid; + gap: 14px; } -.chip, .ghost-chip { +.button { display: inline-flex; align-items: center; justify-content: center; gap: 8px; - border-radius: 999px; - border: 1px solid var(--border); + min-height: 42px; padding: 10px 14px; - background: rgba(255,255,255,0.06); - color: var(--text); + border-radius: 12px; + border: 1px solid var(--border-color); + cursor: pointer; + transition: transform 0.18s ease, border-color 0.18s ease, background 0.18s ease, box-shadow 0.18s ease; } -.ghost-chip { - background: rgba(255,255,255,0.04); +.button:hover { + transform: translateY(-1px); + border-color: var(--border-strong); } -.hero-meta { +.button-primary { + border-color: rgba(77, 141, 255, 0.45); + background: linear-gradient(135deg, rgba(77, 141, 255, 0.94), rgba(90, 121, 255, 0.94)); + color: #fff; + box-shadow: 0 0 24px rgba(77, 141, 255, 0.28); +} + +.button-secondary { + background: rgba(255, 255, 255, 0.05); + color: var(--text-primary); +} + +.toggle-card, +.sync-state, +.hero-links { + border: 1px solid var(--border-color); + border-radius: 14px; + background: rgba(255, 255, 255, 0.04); +} + +.toggle-card { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + padding: 10px 14px; +} + +.toggle-label { + color: var(--text-secondary); + font-size: 0.92rem; +} + +.toggle-switch { + position: relative; + display: inline-flex; + width: 44px; + height: 24px; +} + +.toggle-switch input { + position: absolute; + opacity: 0; + inset: 0; +} + +.toggle-slider { + position: absolute; + inset: 0; + border-radius: 999px; + background: rgba(148, 163, 184, 0.18); + border: 1px solid rgba(148, 163, 184, 0.28); + transition: background 0.18s ease, border-color 0.18s ease; +} + +.toggle-slider::before { + content: ""; + position: absolute; + top: 2px; + left: 2px; + width: 18px; + height: 18px; + border-radius: 999px; + background: #fff; + transition: transform 0.18s ease; +} + +.toggle-switch input:checked + .toggle-slider { + background: rgba(77, 141, 255, 0.95); + border-color: rgba(77, 141, 255, 0.95); +} + +.toggle-switch input:checked + .toggle-slider::before { + transform: translateX(20px); +} + +.sync-state { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 14px; +} + +.sync-state strong { + display: block; + font-size: 0.92rem; +} + +.sync-state span { + display: block; + margin-top: 3px; + color: var(--text-secondary); + font-size: 0.8rem; +} + +.icon-sync { + color: var(--accent-blue); + width: 14px; + height: 14px; + filter: drop-shadow(0 0 10px rgba(77, 141, 255, 0.75)); +} + +.hero-links { + display: flex; + flex-wrap: wrap; + gap: 10px; + padding: 12px; +} + +.metrics-row { + position: relative; display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 14px; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 16px; + margin-top: 22px; } -.meta-card, .glass-panel { - background: var(--panel); - border: 1px solid var(--border); - border-radius: var(--radius); - box-shadow: var(--shadow); +.metric-card { + position: relative; + padding: 16px 18px; + border-radius: 18px; + border: 1px solid var(--border-color); + background: rgba(8, 13, 24, 0.62); + box-shadow: var(--shadow-md); + overflow: hidden; } -.meta-card { - padding: 18px; - min-height: 116px; +.metric-card::before { + content: ""; + position: absolute; + inset: 0 auto 0 0; + width: 4px; + background: var(--metric-color, var(--accent-purple)); } -.meta-card strong { - display: block; - color: var(--muted); - font-size: 0.84rem; - letter-spacing: 0.08em; +.metric-label { + display: flex; + align-items: center; + gap: 8px; + color: var(--text-secondary); text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 0.76rem; } -.meta-card span { - display: block; - margin-top: 10px; - font-size: 2rem; +.metric-value { + margin-top: 14px; + font-size: clamp(1.8rem, 3vw, 2.4rem); font-weight: 700; } -.workspace { +.metric-note { + margin-top: 8px; + color: var(--text-dim); + font-size: 0.82rem; +} + +.main-container { display: grid; - grid-template-columns: 420px minmax(0, 1fr); + grid-template-columns: 320px minmax(0, 1fr); gap: 20px; - margin-top: 22px; + margin-top: 20px; + min-height: calc(100vh - 260px); } .sidebar { display: flex; flex-direction: column; - gap: 18px; + gap: 16px; } -.panel-header { +.sidebar-section, +.panel, +.workspace-empty { + border: 1px solid var(--border-color); + border-radius: 18px; + background: var(--bg-card); + box-shadow: var(--shadow-md); +} + +.sidebar-section { + padding: 16px; +} + +.sidebar-section-fill { + flex: 1 1 auto; + min-height: 260px; +} + +.section-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; - margin-bottom: 16px; -} - -.panel-header h2, .panel-header h3 { - margin: 0; - font-size: 1rem; - letter-spacing: 0.04em; + margin-bottom: 14px; + color: var(--text-secondary); text-transform: uppercase; - color: var(--muted); + letter-spacing: 0.08em; + font-size: 0.76rem; } -.glass-panel { - padding: 18px; - background: - linear-gradient(180deg, rgba(255,255,255,0.04), transparent 35%), - var(--panel); +.section-header span:first-child { + display: inline-flex; + align-items: center; + gap: 8px; } -.filters { +.section-badge, +.tag, +.status-pill, +.section-chip { + display: inline-flex; + align-items: center; + gap: 8px; + min-height: 26px; + padding: 4px 10px; + border-radius: 999px; + border: 1px solid var(--border-color); + font-size: 0.76rem; + white-space: nowrap; +} + +.field { + display: grid; + gap: 8px; + color: var(--text-secondary); + font-size: 0.84rem; +} + +.filter-group { display: grid; gap: 12px; } -.filters label { - display: grid; - gap: 6px; - color: var(--muted); - font-size: 0.9rem; +.search-box, +.filter-select { + display: flex; + align-items: center; + gap: 8px; + min-height: 42px; + padding: 0 12px; + border-radius: 12px; + border: 1px solid var(--border-color); + background: rgba(255, 255, 255, 0.04); + color: var(--text-primary); } -.filters input, .filters select { +.search-box input { width: 100%; - background: rgba(255,255,255,0.05); - color: var(--text); - border: 1px solid rgba(159, 179, 202, 0.18); - border-radius: 14px; - padding: 12px 14px; + border: 0; + outline: none; + background: transparent; + color: inherit; } +.filter-select { + appearance: none; +} + +.system-stats, +.failure-list, .run-list { display: grid; gap: 12px; - max-height: calc(100vh - 460px); +} + +.system-card, +.failure-card, +.run-card, +.plan-card, +.detail-stat, +.artifact-group, +.viewer-card, +.json-card { + border: 1px solid var(--border-color); + border-radius: 14px; + background: rgba(255, 255, 255, 0.03); +} + +.system-card, +.failure-card, +.run-card, +.plan-card, +.detail-stat, +.json-card { + padding: 12px 14px; +} + +.system-title, +.failure-title, +.run-title { + font-weight: 600; +} + +.system-meta, +.failure-reason, +.run-meta, +.muted, +.detail-subtitle, +.plan-copy, +.source-links a, +.timeline-detail, +.timeline-time, +.viewer-meta, +.footer-note, +.empty-copy { + color: var(--text-secondary); +} + +.meter { + height: 8px; + margin-top: 12px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.08); + overflow: hidden; +} + +.meter > span { + display: block; + height: 100%; + width: var(--fill, 0%); + background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple)); +} + +.run-list { + max-height: calc(100vh - 470px); overflow: auto; padding-right: 4px; } .run-card { - width: 100%; - text-align: left; - padding: 16px; - border-radius: 18px; - border: 1px solid rgba(159, 179, 202, 0.14); - background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0.03)); - color: var(--text); cursor: pointer; - transition: transform 180ms ease, border-color 180ms ease, background 180ms ease; + transition: transform 0.18s ease, border-color 0.18s ease, background 0.18s ease; } -.run-card:hover, .run-card.is-active { +.run-card:hover, +.run-card.is-active { transform: translateY(-1px); - border-color: rgba(94, 234, 212, 0.42); - background: linear-gradient(180deg, rgba(94, 234, 212, 0.14), rgba(255,255,255,0.05)); + border-color: rgba(77, 141, 255, 0.42); + background: rgba(77, 141, 255, 0.08); } -.run-card-top, .flex-row { +.run-topline, +.detail-topline, +.viewer-toolbar, +.panel-header, +.detail-actions, +.tag-row, +.timeline-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; } -.run-card h4 { - margin: 10px 0 8px; - font-size: 1rem; - line-height: 1.35; -} - -.mini-muted { - color: var(--muted); - font-size: 0.86rem; +.tag-row { + flex-wrap: wrap; } .status-pill { - display: inline-flex; - align-items: center; - gap: 7px; - border-radius: 999px; - padding: 6px 10px; - font-size: 0.82rem; font-weight: 700; text-transform: uppercase; - letter-spacing: 0.06em; - border: 1px solid transparent; + letter-spacing: 0.07em; } .status-pill::before { @@ -283,446 +543,527 @@ button, input, select { height: 8px; border-radius: 999px; background: currentColor; - box-shadow: 0 0 16px currentColor; -} - -.status-blocked-artifact, .status-blocked-destructive { - color: var(--danger); - background: rgba(255, 123, 123, 0.14); - border-color: rgba(255, 123, 123, 0.24); -} - -.status-triage-manual, .status-suspected { - color: var(--warning); - background: rgba(255, 209, 102, 0.14); - border-color: rgba(255, 209, 102, 0.24); + box-shadow: 0 0 14px currentColor; } .status-verified-real { - color: var(--success); - background: rgba(110, 231, 165, 0.14); - border-color: rgba(110, 231, 165, 0.24); + color: var(--accent-green); + border-color: rgba(30, 212, 157, 0.28); + background: rgba(30, 212, 157, 0.12); } .status-verified-synthetic { - color: var(--accent-3); - background: rgba(144, 205, 244, 0.14); - border-color: rgba(144, 205, 244, 0.24); + color: #8fd8ff; + border-color: rgba(143, 216, 255, 0.28); + background: rgba(143, 216, 255, 0.12); +} + +.status-blocked-artifact, +.status-blocked-destructive { + color: var(--accent-red); + border-color: rgba(255, 107, 122, 0.32); + background: rgba(255, 107, 122, 0.12); +} + +.status-triage-manual, +.status-suspected { + color: var(--accent-yellow); + border-color: rgba(255, 181, 71, 0.28); + background: rgba(255, 181, 71, 0.12); } .status-default { - color: var(--accent); - background: rgba(94, 234, 212, 0.14); - border-color: rgba(94, 234, 212, 0.24); + color: var(--accent-blue); + border-color: rgba(77, 141, 255, 0.28); + background: rgba(77, 141, 255, 0.12); } -.detail-view { +.workspace { + min-width: 0; +} + +.workspace-empty { display: grid; - gap: 18px; + place-items: center; + gap: 10px; + min-height: 320px; + padding: 28px; + text-align: center; +} + +.workspace-empty h2 { + margin: 0; + font-family: "IBM Plex Serif", Georgia, serif; + font-size: 1.8rem; } .detail-hero { - padding: 22px; - overflow: hidden; - position: relative; + padding: 22px 22px 20px; + margin-bottom: 18px; + border: 1px solid var(--border-color); + border-radius: 22px; + background: + linear-gradient(135deg, rgba(17, 24, 42, 0.98) 0%, rgba(22, 17, 44, 0.96) 100%); + box-shadow: var(--shadow-lg); } -.detail-hero::after { - content: ""; - position: absolute; - inset: auto -20% -55% 25%; - height: 220px; - background: radial-gradient(circle, rgba(94, 234, 212, 0.2), transparent 55%); - pointer-events: none; -} - -.detail-headline { - margin: 8px 0 12px; - font-family: "IBM Plex Serif", "Iowan Old Style", Georgia, serif; - font-size: clamp(1.6rem, 3vw, 2.8rem); +.detail-title { + margin: 12px 0 8px; + font-family: "IBM Plex Serif", Georgia, serif; + font-size: clamp(1.7rem, 3vw, 2.6rem); line-height: 1.08; } -.tag-row, .link-row, .artifact-row { - display: flex; +.detail-actions { flex-wrap: wrap; - gap: 10px; -} - -.tag { - display: inline-flex; - align-items: center; - padding: 7px 10px; - border-radius: 999px; - background: rgba(255,255,255,0.06); - border: 1px solid rgba(159, 179, 202, 0.18); - color: var(--text); - font-size: 0.86rem; -} - -.stat-grid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 12px; margin-top: 18px; } -.stat-card { - padding: 14px; - border-radius: 16px; - background: rgba(255,255,255,0.04); - border: 1px solid rgba(159, 179, 202, 0.16); +.detail-stat-grid, +.plan-grid, +.raw-json-grid { + display: grid; + gap: 14px; } -.stat-card strong { +.detail-stat-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + margin-top: 18px; +} + +.detail-stat strong, +.plan-label { display: block; - color: var(--muted); - font-size: 0.78rem; + color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.08em; + font-size: 0.74rem; } -.stat-card span { +.detail-stat span { display: block; margin-top: 10px; - font-size: 1.15rem; + font-size: 1.2rem; font-weight: 700; } -.detail-grid { - display: grid; - grid-template-columns: minmax(0, 1fr) 360px; - gap: 18px; -} - -.stack { - display: grid; - gap: 18px; -} - -.progress-strip { - display: grid; - gap: 12px; +.panel { + overflow: hidden; margin-bottom: 16px; } -.progress-bar { - display: flex; +.panel-header { width: 100%; - min-height: 12px; + padding: 16px 18px; + background: rgba(255, 255, 255, 0.02); + border: 0; + color: inherit; + cursor: pointer; +} + +.panel-title { + display: inline-flex; + align-items: center; + gap: 10px; + font-weight: 700; +} + +.panel-meta { + display: inline-flex; + align-items: center; + gap: 12px; + color: var(--text-secondary); +} + +.panel-chevron { + transition: transform 0.2s ease; +} + +.panel.is-collapsed .panel-chevron { + transform: rotate(-90deg); +} + +.panel-content { + display: grid; + grid-template-rows: 1fr; + transition: grid-template-rows 0.24s ease, opacity 0.24s ease; +} + +.panel-content-inner { + min-height: 0; overflow: hidden; + padding: 0 18px 18px; +} + +.panel.is-collapsed .panel-content { + grid-template-rows: 0fr; + opacity: 0; +} + +.timeline { + display: grid; + gap: 14px; +} + +.timeline-item { + position: relative; + padding-left: 24px; + padding-bottom: 12px; + border-left: 1px solid rgba(148, 163, 184, 0.18); +} + +.timeline-item:last-child { + padding-bottom: 0; +} + +.timeline-dot { + position: absolute; + left: -7px; + top: 4px; + width: 12px; + height: 12px; border-radius: 999px; - background: rgba(255,255,255,0.08); - border: 1px solid rgba(159, 179, 202, 0.14); + border: 2px solid currentColor; + background: var(--bg-dark); +} + +.timeline-success { + color: var(--accent-green); +} + +.timeline-blocked, +.timeline-failed { + color: var(--accent-red); +} + +.timeline-pending { + color: var(--accent-blue); +} + +.timeline-neutral { + color: var(--text-dim); +} + +.timeline-head strong { + font-size: 0.96rem; +} + +.timeline-time { + font-size: 0.76rem; +} + +.timeline-detail { + margin-top: 6px; + line-height: 1.55; + font-size: 0.9rem; +} + +.progress-bar { + height: 10px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.07); + overflow: hidden; + margin-bottom: 14px; } .progress-segment { - min-width: 10px; - transition: width 180ms ease; + height: 100%; + float: left; } -.progress-completed { background: linear-gradient(90deg, rgba(110, 231, 165, 0.9), rgba(94, 234, 212, 0.9)); } -.progress-blocked { background: linear-gradient(90deg, rgba(255, 123, 123, 0.95), rgba(255, 160, 122, 0.9)); } -.progress-failed { background: linear-gradient(90deg, rgba(255, 123, 123, 0.92), rgba(255, 209, 102, 0.88)); } -.progress-skipped { background: linear-gradient(90deg, rgba(255,255,255,0.22), rgba(159, 179, 202, 0.3)); } -.progress-planned { background: linear-gradient(90deg, rgba(144, 205, 244, 0.82), rgba(94, 234, 212, 0.72)); } -.progress-other { background: linear-gradient(90deg, rgba(255,255,255,0.18), rgba(255,255,255,0.1)); } +.progress-completed { + background: linear-gradient(90deg, var(--accent-green), #54f0bf); +} + +.progress-blocked { + background: linear-gradient(90deg, var(--accent-red), #ff95a0); +} + +.progress-failed { + background: linear-gradient(90deg, #ff8a47, var(--accent-red)); +} + +.progress-skipped { + background: rgba(148, 163, 184, 0.35); +} + +.progress-planned { + background: linear-gradient(90deg, var(--accent-blue), #8fbaff); +} + +.progress-other { + background: rgba(148, 163, 184, 0.2); +} .progress-legend { display: flex; flex-wrap: wrap; gap: 10px; + margin-bottom: 16px; } -.progress-legend .tag { - gap: 7px; -} - -.progress-legend .swatch { +.swatch { width: 10px; height: 10px; border-radius: 999px; - display: inline-block; } -.stage-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 12px; - margin-bottom: 18px; +.plan-grid, +.raw-json-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); } -.stage-card { - padding: 14px; - border-radius: 16px; - background: rgba(255,255,255,0.04); - border: 1px solid rgba(159, 179, 202, 0.16); +.plan-copy { + margin-top: 8px; + line-height: 1.55; + font-size: 0.92rem; } -.stage-card strong { - display: block; - margin-bottom: 10px; -} - -.accordion { - overflow: hidden; -} - -.accordion > summary { - list-style: none; - cursor: pointer; - padding: 18px 20px; - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; -} - -.accordion > summary::-webkit-details-marker { display: none; } -.accordion > summary span { - font-size: 1rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--muted); -} - -.accordion .accordion-content { - padding: 0 20px 20px; - border-top: 1px solid rgba(159, 179, 202, 0.12); -} - -.timeline-list { - display: grid; - gap: 12px; -} - -.timeline-item { - display: grid; - grid-template-columns: 120px 180px minmax(0, 1fr); - gap: 12px; - padding: 12px 0; - border-bottom: 1px solid rgba(159, 179, 202, 0.12); -} - -.timeline-item:last-child { - border-bottom: 0; -} - -.timeline-step { - font-weight: 700; -} - -.artifact-group { +.callout { + padding: 14px 16px; + border-radius: 14px; + border: 1px solid rgba(255, 107, 122, 0.28); + background: rgba(255, 107, 122, 0.12); margin-bottom: 14px; } -.artifact-group h4 { - margin: 0 0 10px; - color: var(--muted); - font-size: 0.88rem; +.artifact-groups { + display: grid; + gap: 16px; +} + +.artifact-group h3 { + margin: 0 0 12px; + font-size: 0.86rem; + color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.08em; } -.artifact-button { - display: inline-flex; - align-items: center; - gap: 8px; - margin: 0 10px 10px 0; - padding: 10px 12px; - border-radius: 14px; - border: 1px solid rgba(159, 179, 202, 0.16); - background: rgba(255,255,255,0.05); - color: var(--text); - cursor: pointer; -} - -.artifact-button:hover, .artifact-button.is-active { - border-color: rgba(94, 234, 212, 0.4); - background: rgba(94, 234, 212, 0.12); -} - -.log-viewer { - min-height: 420px; +.artifact-grid { display: grid; - gap: 14px; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 12px; } -.viewer-toolbar { - display: flex; - flex-wrap: wrap; - justify-content: space-between; - gap: 10px; - align-items: center; -} - -.viewer-frame { - background: rgba(2, 8, 22, 0.88); - border: 1px solid rgba(159, 179, 202, 0.18); - border-radius: 16px; - min-height: 300px; - overflow: hidden; -} - -.viewer-frame pre { - margin: 0; - padding: 18px; - max-height: 560px; - overflow: auto; - font-family: "IBM Plex Mono", "SFMono-Regular", "Menlo", monospace; - font-size: 0.88rem; - line-height: 1.6; - color: #d6e5f5; - white-space: pre-wrap; -} - -.viewer-frame img { - display: block; +.artifact-button { + display: grid; + gap: 8px; width: 100%; - height: auto; + padding: 12px; + text-align: left; + border: 1px solid var(--border-color); + border-radius: 14px; + background: rgba(255, 255, 255, 0.04); + color: inherit; + cursor: pointer; + transition: transform 0.18s ease, border-color 0.18s ease, background 0.18s ease; +} + +.artifact-button:hover, +.artifact-button.is-active { + transform: translateY(-1px); + border-color: rgba(77, 141, 255, 0.42); + background: rgba(77, 141, 255, 0.08); +} + +.artifact-kind { + color: var(--text-dim); + font-size: 0.78rem; } .gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 14px; + gap: 12px; + margin-top: 12px; } -.gallery button { - all: unset; - cursor: pointer; - border-radius: 18px; +.gallery-card { + display: block; + width: 100%; + padding: 0; overflow: hidden; - border: 1px solid rgba(159, 179, 202, 0.18); - background: rgba(255,255,255,0.04); } -.gallery img { +.gallery-card img { display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover; } -.gallery figcaption { - padding: 10px 12px 14px; - color: var(--muted); - font-size: 0.84rem; +.gallery-card span { + display: block; + padding: 10px 12px 12px; } -.failure-callout { - padding: 16px 18px; - border-radius: 18px; - border: 1px solid rgba(255, 123, 123, 0.2); - background: rgba(255, 123, 123, 0.09); +.viewer-card { + padding: 14px; } -.json-block { - background: rgba(2, 8, 22, 0.72); - border-radius: 16px; - border: 1px solid rgba(159, 179, 202, 0.14); +.viewer-toolbar { + flex-wrap: wrap; + margin-bottom: 14px; +} + +.viewer-label { + font-size: 1rem; + font-weight: 600; +} + +.viewer-meta { + margin-top: 4px; + font-size: 0.82rem; + word-break: break-all; +} + +.viewer-frame { + min-height: 320px; + border: 1px solid rgba(148, 163, 184, 0.16); + border-radius: 14px; + background: rgba(5, 9, 18, 0.92); + overflow: hidden; +} + +.viewer-frame pre { + margin: 0; + max-height: 560px; padding: 16px; overflow: auto; - font-family: "IBM Plex Mono", "SFMono-Regular", monospace; + color: #dbe7ff; + font-family: "IBM Plex Mono", "SFMono-Regular", Menlo, monospace; font-size: 0.84rem; + line-height: 1.6; + white-space: pre-wrap; +} + +.viewer-frame img { + display: block; + max-width: 100%; + height: auto; +} + +.viewer-frame iframe { + display: block; + width: 100%; + min-height: 580px; + border: 0; + background: #fff; +} + +.source-links { + display: grid; + gap: 10px; + margin-top: 14px; +} + +.source-links a { + text-decoration: underline; + text-underline-offset: 3px; +} + +.json-card pre { + margin: 0; + max-height: 420px; + overflow: auto; + color: #dbe7ff; + font-family: "IBM Plex Mono", "SFMono-Regular", Menlo, monospace; + font-size: 0.82rem; line-height: 1.55; - color: #c9d8e8; + white-space: pre-wrap; } -.empty-state { - padding: 40px 24px; - text-align: center; - color: var(--muted); +.dashboard-footer { + display: flex; + justify-content: space-between; + gap: 16px; + align-items: center; + margin-top: 16px; + padding: 14px 6px 0; } -.failure-feed { - display: grid; - gap: 10px; -} - -.failure-item { - padding: 12px 14px; - border-radius: 16px; - background: rgba(255,255,255,0.04); - border: 1px solid rgba(159, 179, 202, 0.16); -} - -.system-grid { - display: grid; - gap: 10px; -} - -.system-card { - padding: 14px 16px; - border-radius: 16px; - background: rgba(255,255,255,0.04); - border: 1px solid rgba(159, 179, 202, 0.14); -} - -.meter { - position: relative; - height: 10px; - border-radius: 999px; - background: rgba(255,255,255,0.08); - overflow: hidden; - margin-top: 10px; -} - -.meter > span { - position: absolute; - inset: 0 auto 0 0; - width: var(--fill, 0%); - background: linear-gradient(90deg, var(--accent), var(--accent-2)); - border-radius: inherit; -} - -.sync-indicator { +.footer-note { display: inline-flex; align-items: center; gap: 8px; + font-size: 0.84rem; } -.sync-indicator strong { - color: var(--text); +.footer-links { + display: flex; + flex-wrap: wrap; + gap: 12px; + font-size: 0.84rem; + color: var(--text-secondary); } -.dot { - width: 10px; - height: 10px; - border-radius: 999px; - background: var(--accent); - box-shadow: 0 0 18px rgba(94, 234, 212, 0.8); +.footer-links a { + text-decoration: underline; + text-underline-offset: 3px; } -@keyframes pulse { - 0%, 100% { transform: scale(1); opacity: 0.88; } - 50% { transform: scale(1.35); opacity: 1; } +.empty-state { + padding: 24px; + text-align: center; + color: var(--text-secondary); } -@media (max-width: 1280px) { - .workspace, .detail-grid, .hero-grid { +.empty-copy { + line-height: 1.55; +} + +@media (max-width: 1320px) { + .hero-top, + .main-container, + .detail-stat-grid, + .plan-grid, + .raw-json-grid { grid-template-columns: 1fr; } - .stat-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); + .sidebar { + order: 2; + } + + .workspace { + order: 1; } } -@media (max-width: 760px) { +@media (max-width: 960px) { .dashboard-shell { - padding: 18px 14px 32px; + padding: 14px 14px 22px; } .hero { position: static; + padding: 18px; } - .stat-grid, .hero-meta { - grid-template-columns: 1fr; + .metrics-row { + grid-template-columns: repeat(2, minmax(0, 1fr)); } - .timeline-item { - grid-template-columns: 1fr; + .hero-links, + .detail-actions, + .tag-row, + .panel-meta, + .viewer-toolbar, + .dashboard-footer { + flex-direction: column; + align-items: stretch; + } +} + +@media (max-width: 640px) { + .metrics-row, + .artifact-grid, + .gallery { + grid-template-columns: 1fr; + } + + .hero-copy h1 { + font-size: 1.85rem; } } diff --git a/08-threat-intel/generated/dashboard/docs/design-source.html b/08-threat-intel/generated/dashboard/docs/design-source.html new file mode 100644 index 00000000..8ca93e8f --- /dev/null +++ b/08-threat-intel/generated/dashboard/docs/design-source.html @@ -0,0 +1,111 @@ + + + + + + Lovart 设计来源与本地化清单 + + + +
+
+ +

Lovart 设计来源与本地化清单

+
Local vendor manifest for the Lovart-derived dashboard shell.
+
{
+  "template_id": "lovart-authorized-lab-dashboard",
+  "source_url": "https://assets-persist.lovart.ai/agent_images/464011bb-fbbc-4bd4-98f8-90897dd43612.html",
+  "downloaded_at": "2026-03-17T07:56:29Z",
+  "original_filename": "464011bb-fbbc-4bd4-98f8-90897dd43612.html",
+  "vendor_source_path": "scripts/lab/dashboard_templates/lovart/vendor/464011bb-fbbc-4bd4-98f8-90897dd43612.html",
+  "runtime_template": {
+    "index": "scripts/lab/dashboard_templates/lovart/index.html",
+    "styles": "scripts/lab/dashboard_templates/lovart/assets/styles.css",
+    "app": "scripts/lab/dashboard_templates/lovart/assets/app.js",
+    "icons": "scripts/lab/dashboard_templates/lovart/assets/icons.svg"
+  },
+  "notes": [
+    "The remote Lovart HTML is tracked for provenance only and is not used at runtime.",
+    "Runtime assets are localized into repository-managed templates and generated output.",
+    "External fonts and icon CDNs are intentionally removed from the generated dashboard."
+  ]
+}
+
+
+
+ + diff --git a/08-threat-intel/generated/dashboard/docs/frontend-dashboard-design.html b/08-threat-intel/generated/dashboard/docs/frontend-dashboard-design.html index 68f975cf..24033056 100644 --- a/08-threat-intel/generated/dashboard/docs/frontend-dashboard-design.html +++ b/08-threat-intel/generated/dashboard/docs/frontend-dashboard-design.html @@ -85,7 +85,7 @@ Back to dashboard

本地前端工作台设计文档

-
Dashboard-local mirror of the UI and interaction specification. | source: docs/frontend-dashboard-design.md
+
Dashboard-local mirror of the UI and interaction specification.
# 本地前端工作台设计文档
 
 > `LAB ONLY` | `AUTHORIZED TARGETS ONLY`
@@ -119,6 +119,8 @@
 - 信息密度高,但必须可折叠、可筛选、可逐层展开
 - 日志与原始 JSON 必须能直接预览
 - 页面视觉应更生动,但不能牺牲扫描效率
+- 默认路由采用正式新 UI,同时保留 `legacy` 回退入口
+- 运行期不得依赖外部 HTML、字体 CDN 或图标 CDN
 
 ## 3. 信息架构
 
@@ -376,7 +378,22 @@ flowchart LR
 
 前端不直接写这些数据,只读取并展示。
 
-## 10. 落地约束
+## 10. 路由与文档地址
+
+- `/index.html`
+  - 默认正式入口,使用本地化 Lovart UI 外壳
+- `/legacy/index.html`
+  - 旧版 dashboard 回退入口
+- `/docs/project-features.html`
+  - 功能说明镜像页
+- `/docs/frontend-dashboard-design.html`
+  - 设计说明镜像页
+- `/docs/secure-code-index.html`
+  - secure-code 索引镜像页
+- `/docs/design-source.html`
+  - Lovart vendor 来源和本地化说明
+
+## 11. 落地约束
 
 - 保持静态前端,不引入长期运行后端
 - 本地 `serve-dashboard` 即可查看
@@ -384,7 +401,7 @@ flowchart LR
 - 不依赖第三方 CDN UI 库
 - 优先使用原生 HTML/CSS/JS,可长期维护
 
-## 11. 验收标准
+## 12. 验收标准
 
 页面完成后,应满足:
 
diff --git a/08-threat-intel/generated/dashboard/docs/project-features.html b/08-threat-intel/generated/dashboard/docs/project-features.html
index 59bca206..2389af06 100644
--- a/08-threat-intel/generated/dashboard/docs/project-features.html
+++ b/08-threat-intel/generated/dashboard/docs/project-features.html
@@ -85,7 +85,7 @@
         Back to dashboard
       
       

项目功能与特性总览

-
Dashboard-local mirror of the repo feature guide. | source: docs/project-features.md
+
Dashboard-local mirror of the repo feature guide.
# 项目功能与特性总览
 
 > `LAB ONLY` | `AUTHORIZED TARGETS ONLY`
@@ -149,6 +149,9 @@
   - `report.md`, `report.html`, `timeline.mmd`, `assets/`, `logs/`
 - `08-threat-intel/generated/dashboard/`
   - 静态前端工作台
+  - `/index.html` 为本地化 Lovart 正式 UI
+  - `/legacy/index.html` 为旧版工作台回退入口
+  - `/docs/*.html` 为本地可访问的说明与设计镜像页
 - `07-framework-security/`
   - 系统级 README、INDEX、案例页,自动显示本地实证状态
 
@@ -248,6 +251,15 @@ python3 /Users/x/websafe/scripts/lab/main.py serve-dashboard --port 8734
 详细设计见:
 
 - [本地前端工作台设计文档](/Users/x/websafe/docs/frontend-dashboard-design.md)
+
+当前地址布局固定为:
+
+- `/index.html`
+  - 默认新 UI,基于本地化 Lovart 视觉壳层,绑定真实 dashboard JSON
+- `/legacy/index.html`
+  - 旧版工作台显式保留,用于快速回退和对照
+- `/docs/design-source.html`
+  - 设计来源、vendor manifest 与本地化说明
 
diff --git a/08-threat-intel/generated/dashboard/docs/secure-code-index.html b/08-threat-intel/generated/dashboard/docs/secure-code-index.html index c33a4148..1dbffe38 100644 --- a/08-threat-intel/generated/dashboard/docs/secure-code-index.html +++ b/08-threat-intel/generated/dashboard/docs/secure-code-index.html @@ -85,7 +85,7 @@ Back to dashboard

安全编码修复库索引

-
Dashboard-local mirror of the secure-code library index. | source: 05-defense/secure-code/INDEX.md
+
Dashboard-local mirror of the secure-code library index.
# 安全编码修复库
 
 > `LAB ONLY` | 修复主题用于把实验发现映射回代码整改,不代表默认生产基线。
diff --git a/08-threat-intel/generated/dashboard/index.html b/08-threat-intel/generated/dashboard/index.html
index 3bbf6390..a7d919a3 100644
--- a/08-threat-intel/generated/dashboard/index.html
+++ b/08-threat-intel/generated/dashboard/index.html
@@ -1,81 +1,168 @@
-
 
 
 
   
   
-  websafe authorized lab dashboard
+  Authorized Lab Dashboard
   
 
 
+  
   
-
-
-
Authorized Lab Dashboard
+ + + +
+
+
+ + Authorized Lab Dashboard +

本地攻防实证工作台

-

面向授权实验场景的本地静态前端。聚合 advisory、run bundle、日志、浏览器证据、失败原因、利用思路与源头信息,并支持可折叠细节与自动刷新。

-
- - - Open Summary JSON - Open Feature Docs -
+

+ Lovart 设计外壳已本地化并接入真实 run bundle 数据。页面只面向授权实验资产, + 聚合 advisory、timeline、evidence、logs、sources、raw JSON 与失败原因。 +

-
-
-

Sync State

-
BootingLoading generated JSON
+ +
+ + +
+ +
+ Booting + Loading generated JSON +
+
+ -
+ +
-
+
-
-
Select a run to inspect full details.
-
-
+
+
+ +

Select a run

+

Pick a run from the left queue to inspect timeline, evidence, logs, sources and raw JSON.

+
+
+ + +
+ diff --git a/08-threat-intel/generated/dashboard/legacy/advisories.json b/08-threat-intel/generated/dashboard/legacy/advisories.json new file mode 120000 index 00000000..eea4bfec --- /dev/null +++ b/08-threat-intel/generated/dashboard/legacy/advisories.json @@ -0,0 +1 @@ +../advisories.json \ No newline at end of file diff --git a/08-threat-intel/generated/dashboard/legacy/assets/app.js b/08-threat-intel/generated/dashboard/legacy/assets/app.js new file mode 100644 index 00000000..7710b047 --- /dev/null +++ b/08-threat-intel/generated/dashboard/legacy/assets/app.js @@ -0,0 +1,573 @@ + +const state = { + summary: null, + runs: [], + systems: [], + advisories: {}, + profiles: {}, + selectedRunId: null, + selectedArtifact: null, + filters: { search: "", system: "", status: "", family: "" }, + autoRefresh: true, + refreshMs: 5000, + refreshHandle: null, +}; + +const $ = (id) => document.getElementById(id); +const statusClass = (status) => `status-pill ${({ + "blocked-artifact": "status-blocked-artifact", + "blocked-destructive": "status-blocked-destructive", + "triage-manual": "status-triage-manual", + "verified-real": "status-verified-real", + "verified-synthetic": "status-verified-synthetic", + "suspected": "status-suspected", + "completed": "status-verified-real", + "failed": "status-blocked-artifact", + "skipped": "status-triage-manual" +})[status] || "status-default"}`; + +function escapeHtml(value) { + return String(value ?? "") + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """); +} + +function timeAgo(value) { + if (!value) return "-"; + const diff = Date.now() - new Date(value).getTime(); + if (Number.isNaN(diff)) return value; + const seconds = Math.floor(diff / 1000); + if (seconds < 60) return `${seconds}s ago`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; +} + +async function fetchJson(url) { + const response = await fetch(`${url}?t=${Date.now()}`, { cache: "no-store" }); + if (!response.ok) { + throw new Error(`${url} -> ${response.status}`); + } + return response.json(); +} + +async function loadData(preserveSelection = true) { + $("syncState").innerHTML = `Refreshing${new Date().toLocaleTimeString()}`; + const previousRun = state.selectedRunId; + try { + const [summary, runs, systems, advisories, profiles] = await Promise.all([ + fetchJson("./summary.json"), + fetchJson("./runs.json"), + fetchJson("./systems.json"), + fetchJson("./advisories.json"), + fetchJson("./profiles.json"), + ]); + state.summary = summary; + state.runs = runs; + state.systems = systems; + state.advisories = advisories; + state.profiles = profiles; + hydrateFilterOptions(); + + const hashRun = location.hash.startsWith("#run=") ? location.hash.replace("#run=", "") : null; + const selectedCandidate = preserveSelection ? (hashRun || previousRun) : hashRun; + if (selectedCandidate && runs.some((item) => item.run_id === selectedCandidate)) { + state.selectedRunId = selectedCandidate; + } else { + state.selectedRunId = runs[0]?.run_id || null; + } + + renderDashboard(); + $("syncState").innerHTML = `Live${summary.generated_at || new Date().toISOString()}`; + } catch (error) { + $("syncState").innerHTML = `Load Failed${escapeHtml(error.message)}`; + $("runList").innerHTML = `
Dashboard load failed: ${escapeHtml(error.message)}
`; + $("detailRoot").innerHTML = `
Unable to load dashboard data. Check generated JSON and local static server state.
`; + } +} + +function filteredRuns() { + return state.runs.filter((item) => { + if (state.filters.system && item.system_id !== state.filters.system) return false; + if (state.filters.status && item.verification_status !== state.filters.status) return false; + if (state.filters.family && item.repro_profile_id !== state.filters.family) return false; + if (!state.filters.search) return true; + const advisoryTitle = item.advisory_meta?.title || ""; + const haystack = [item.run_id, item.advisory_id, item.system_id, item.repro_profile_id, advisoryTitle] + .join(" ") + .toLowerCase(); + return haystack.includes(state.filters.search); + }); +} + +function renderMetrics() { + const metrics = [ + { label: "Advisories", value: state.summary?.advisory_count ?? 0 }, + { label: "Run Bundles", value: state.summary?.run_count ?? 0 }, + ...Object.entries(state.summary?.statuses || {}).map(([label, value]) => ({ label, value })), + ]; + $("metrics").innerHTML = metrics + .map((item) => `
${escapeHtml(item.label)}${escapeHtml(item.value)}
`) + .join(""); +} + +function renderSystemCoverage() { + $("systemCoverage").innerHTML = state.systems + .map((system) => { + const total = Math.max(system.total || 0, 1); + const verified = (system.verified_real || 0) + (system.verified_synthetic || 0); + const fill = Math.round((verified / total) * 100); + return ` +
+
+ ${escapeHtml(system.display_name || system.system_id)} + ${escapeHtml(system.browser_present || 0)}/${escapeHtml(system.browser_required || 0)} browser +
+
${escapeHtml(system.system_id)} · latest ${escapeHtml(system.latest_update || "-")}
+
+ real ${escapeHtml(system.verified_real || 0)} + synthetic ${escapeHtml(system.verified_synthetic || 0)} + blocked ${escapeHtml(system.blocked || 0)} + manual ${escapeHtml(system.manual || 0)} +
+
+
+ `; + }) + .join(""); +} + +function renderFailures() { + const failures = state.summary?.recent_failures || []; + $("failureFeed").innerHTML = failures.length + ? failures + .map((item) => ` +
+
+ ${escapeHtml(item.run_id)} + ${escapeHtml(item.status)} +
+
${escapeHtml(item.title || item.advisory_id)}
+
${escapeHtml(item.blocked_reason || "-")}
+
+ `) + .join("") + : `
No recent blockers.
`; +} + +function renderRunList() { + const filtered = filteredRuns(); + $("runCount").textContent = `${filtered.length} shown`; + $("runList").innerHTML = filtered.length + ? filtered + .map((item) => { + const active = item.run_id === state.selectedRunId ? "is-active" : ""; + const title = item.advisory_meta?.title || item.advisory_id; + const reasoning = item.reasoning_lines?.[0] || item.blocked_reason || ""; + const browserLabel = item.browser_evidence?.present ? "ready" : (item.browser_evidence?.required ? "required" : "n/a"); + return ` + + `; + }) + .join("") + : `
No runs match the current filters.
`; + + document.querySelectorAll("[data-run-id]").forEach((button) => { + button.addEventListener("click", () => { + state.selectedRunId = button.dataset.runId; + location.hash = `run=${state.selectedRunId}`; + renderRunList(); + renderDetail(); + }); + }); +} + +function renderDashboard() { + renderMetrics(); + renderSystemCoverage(); + renderFailures(); + renderRunList(); + renderDetail(); +} + +function setFilterListeners() { + [["searchInput", "search"], ["systemFilter", "system"], ["statusFilter", "status"], ["familyFilter", "family"]].forEach(([id, key]) => { + $(id).addEventListener("input", (event) => { + state.filters[key] = String(event.target.value || "").trim().toLowerCase(); + if (key !== "search") { + state.filters[key] = String(event.target.value || ""); + } + renderRunList(); + }); + }); +} + +function hydrateFilterOptions() { + const distinct = (items) => [...new Set(items.filter(Boolean))].sort(); + const patchOptions = (id, values) => { + const control = $(id); + const current = control.value; + control.innerHTML = control.dataset.base; + control.innerHTML += distinct(values).map((value) => ``).join(""); + control.value = current; + }; + patchOptions("systemFilter", state.runs.map((item) => item.system_id)); + patchOptions("statusFilter", state.runs.map((item) => item.verification_status)); + patchOptions("familyFilter", state.runs.map((item) => item.repro_profile_id)); +} + +function defaultArtifact(run) { + const preference = ["attack", "requests", "container", "browser", "baseline", "compose", "reports"]; + for (const key of preference) { + const group = (run.artifact_groups || []).find((item) => item.key === key && item.items?.length); + if (!group) continue; + const preferredText = group.items.find((item) => item.kind === "text"); + return preferredText || group.items[0]; + } + return null; +} + +function totalProgress(progress) { + const values = Object.values(progress || {}).map((value) => Number(value || 0)); + return values.reduce((sum, value) => sum + value, 0); +} + +function renderProgressStrip(progress) { + const total = totalProgress(progress); + if (!total) { + return ` +
+
+
No timeline progress recorded.
+
+ `; + } + const order = [ + ["completed", "Completed", "progress-completed"], + ["blocked", "Blocked", "progress-blocked"], + ["failed", "Failed", "progress-failed"], + ["skipped", "Skipped", "progress-skipped"], + ["planned", "Planned", "progress-planned"], + ["other", "Other", "progress-other"], + ]; + const segments = order + .filter(([key]) => Number(progress?.[key] || 0) > 0) + .map(([key, _label, klass]) => { + const count = Number(progress?.[key] || 0); + const pct = Math.max((count / total) * 100, 4); + return `
`; + }) + .join(""); + const legend = order + .filter(([key]) => Number(progress?.[key] || 0) > 0) + .map(([key, label, klass]) => ` + + + ${escapeHtml(label)} ${escapeHtml(progress?.[key] || 0)} + + `) + .join(""); + return ` +
+
${segments}
+
${legend}
+
+ `; +} + +function renderStageCards(run) { + const timeline = run.timeline || []; + if (!timeline.length) { + return `
No stage records available.
`; + } + return ` +
+ ${timeline.map((item) => ` +
+ ${escapeHtml(item.step || "-")} +
${escapeHtml(item.status || "unknown")}
+
${escapeHtml(item.detail || "-")}
+
${escapeHtml(item.at || "-")}
+
+ `).join("")} +
+ `; +} + +async function openArtifact(href, label, kind) { + state.selectedArtifact = { href, label, kind }; + document.querySelectorAll(".artifact-button").forEach((button) => { + button.classList.toggle("is-active", button.dataset.href === href); + }); + $("artifactLabel").textContent = label; + $("artifactOpen").href = href; + $("artifactMeta").textContent = href; + try { + if (kind === "image") { + $("artifactViewer").innerHTML = `${escapeHtml(label)}`; + return; + } + if (href.endsWith(".html")) { + $("artifactViewer").innerHTML = ``; + return; + } + const response = await fetch(`${href}?t=${Date.now()}`, { cache: "no-store" }); + if (!response.ok) throw new Error(`${href} -> ${response.status}`); + const text = await response.text(); + let formatted = text; + if (href.endsWith(".json")) { + try { + formatted = JSON.stringify(JSON.parse(text), null, 2); + } catch (_error) { + } + } + $("artifactViewer").innerHTML = `
${escapeHtml(formatted)}
`; + } catch (error) { + $("artifactViewer").innerHTML = `
Artifact load failed: ${escapeHtml(error.message)}
`; + } +} + +function renderDetail() { + const run = state.runs.find((item) => item.run_id === state.selectedRunId); + if (!run) { + $("detailRoot").innerHTML = `
Select a run to inspect full timeline, logs, sources, and reasoning.
`; + return; + } + + const advisory = run.advisory_meta || {}; + const profile = run.profile_meta || {}; + const screenshotItems = (run.artifact_groups || []) + .find((group) => group.key === "browser") + ?.items.filter((item) => item.kind === "image") || []; + + $("detailRoot").innerHTML = ` +
+
Local Verification Workspace
+
+ ${escapeHtml(run.verification_status)} +
+ ${escapeHtml(run.system_id)} + ${escapeHtml(run.repro_profile_id)} + ${escapeHtml(run.artifact_mode)} + ${escapeHtml(run.verification_mode)} + ${escapeHtml(run.target_env || "local-docker")} +
+
+

${escapeHtml(advisory.title || run.advisory_id)}

+

${escapeHtml(advisory.summary || "No summary available.")}

+ +
+
Timeline Steps${escapeHtml(run.timeline?.length || 0)}
+
Artifacts${escapeHtml((run.artifact_groups || []).reduce((sum, group) => sum + group.count, 0))}
+
Browser${run.browser_evidence?.present ? "Ready" : (run.browser_evidence?.required ? "Required" : "Optional")}
+
Finished${escapeHtml(timeAgo(run.finished_at))}
+
+
+ +
+
+
+ Progress Timeline${escapeHtml(run.timeline?.length || 0)} steps +
+ ${renderProgressStrip(run.progress)} + ${renderStageCards(run)} +
+ ${(run.timeline || []).map((item) => ` +
+
${escapeHtml(item.at || "-")}
+
${escapeHtml(item.step || "-")}
+
+
${escapeHtml(item.status || "unknown")}
+
${escapeHtml(item.detail || "-")}
+
+
+ `).join("") || `
No timeline items available.
`} +
+
+
+ +
+ Attack Plan & Reasoning${escapeHtml(profile.vuln_family || "unknown")} +
+ ${run.blocked_reason ? `
Failure reason
${escapeHtml(run.blocked_reason)}
` : ""} +
+ destructive risk ${escapeHtml(profile.destructive_risk || "-")} + cleanup ${escapeHtml(profile.cleanup_policy || "-")} + targets ${(profile.allowed_target_types || []).join(", ") || "-"} +
+
+ ${(run.reasoning_lines || []).map((line) => `
${escapeHtml(line)}
`).join("")} +
+
+ ${(profile.success_criteria || []).map((line) => `${escapeHtml(line)}`).join("")} +
+
+
+ +
+ Evidence Explorer${escapeHtml((run.artifact_groups || []).length)} groups +
+ ${(run.artifact_groups || []).map((group) => ` +
+

${escapeHtml(group.label)} · ${escapeHtml(group.count)}

+
+ ${group.items.map((item) => ` + + `).join("")} +
+
+ `).join("") || `
No artifacts linked for this run.
`} + ${screenshotItems.length ? ` + + ` : ""} +
+
+ +
+ Live Log Viewer${state.selectedArtifact ? "active" : "idle"} +
+
+
+
+ ${escapeHtml(state.selectedArtifact?.label || "Select an artifact")} +
${escapeHtml(state.selectedArtifact?.href || "Artifacts and logs can be previewed here.")}
+
+
+ Open artifact + +
+
+
Select a report, log, JSON, screenshot, or timeline file to preview it here.
+
+
+
+
+ +
+
+ Sources & Fix Topics${escapeHtml((advisory.secondary_source_urls || []).length + (advisory.official_source_url ? 1 : 0))} links +
+
+ ${(advisory.aliases || []).map((alias) => `${escapeHtml(alias)}`).join("")} +
+
+ ${advisory.official_source_url ? `${escapeHtml(advisory.official_source_url)}` : `
No official source linked.
`} + ${(advisory.secondary_source_urls || []).map((ref) => `${escapeHtml(ref)}`).join("")} +
+
+ ${(advisory.secure_code_topics || []).map((topic) => `${escapeHtml(topic)}`).join("")} +
+
+
+ +
+ Run JSONraw +
${escapeHtml(JSON.stringify(run, null, 2))}
+
+ +
+ Advisory JSONraw +
${escapeHtml(JSON.stringify(advisory, null, 2))}
+
+ +
+ Profile JSONraw +
${escapeHtml(JSON.stringify(profile, null, 2))}
+
+
+
+ `; + + document.querySelectorAll(".artifact-button").forEach((button) => { + button.addEventListener("click", () => openArtifact(button.dataset.href, button.dataset.label, button.dataset.kind)); + }); + + $("refreshArtifact")?.addEventListener("click", () => { + if (state.selectedArtifact) { + openArtifact(state.selectedArtifact.href, state.selectedArtifact.label, state.selectedArtifact.kind); + } + }); + + if (!state.selectedArtifact || !(run.artifact_groups || []).some((group) => group.items.some((item) => item.href === state.selectedArtifact.href))) { + const artifact = defaultArtifact(run); + if (artifact) { + openArtifact(artifact.href, artifact.label, artifact.kind); + } + } else { + openArtifact(state.selectedArtifact.href, state.selectedArtifact.label, state.selectedArtifact.kind); + } +} + +function attachGlobalActions() { + $("searchInput").addEventListener("input", (event) => { + state.filters.search = String(event.target.value || "").trim().toLowerCase(); + renderRunList(); + }); + [["systemFilter", "system"], ["statusFilter", "status"], ["familyFilter", "family"]].forEach(([id, key]) => { + $(id).addEventListener("input", (event) => { + state.filters[key] = String(event.target.value || ""); + renderRunList(); + }); + }); + $("refreshDashboard").addEventListener("click", () => loadData(false)); + $("autoRefresh").addEventListener("change", (event) => { + state.autoRefresh = Boolean(event.target.checked); + startRefreshLoop(); + }); +} + +function startRefreshLoop() { + if (state.refreshHandle) { + clearInterval(state.refreshHandle); + state.refreshHandle = null; + } + if (!state.autoRefresh) return; + state.refreshHandle = setInterval(() => loadData(true), state.refreshMs); +} + +async function init() { + ["systemFilter", "statusFilter", "familyFilter"].forEach((id) => { + $(id).dataset.base = $(id).innerHTML; + }); + attachGlobalActions(); + await loadData(false); + startRefreshLoop(); + window.addEventListener("hashchange", () => loadData(false)); +} + +document.addEventListener("DOMContentLoaded", init); diff --git a/08-threat-intel/generated/dashboard/legacy/assets/styles.css b/08-threat-intel/generated/dashboard/legacy/assets/styles.css new file mode 100644 index 00000000..61c46ff2 --- /dev/null +++ b/08-threat-intel/generated/dashboard/legacy/assets/styles.css @@ -0,0 +1,728 @@ + +:root { + --bg: #07111f; + --panel: rgba(9, 18, 32, 0.86); + --panel-2: rgba(10, 24, 44, 0.92); + --panel-soft: rgba(18, 32, 56, 0.74); + --border: rgba(137, 171, 214, 0.22); + --text: #f7fafc; + --muted: #9fb3ca; + --accent: #5eead4; + --accent-2: #ffb86b; + --accent-3: #90cdf4; + --danger: #ff7b7b; + --warning: #ffd166; + --success: #6ee7a5; + --shadow: 0 24px 80px rgba(1, 7, 20, 0.45); + --radius: 20px; +} + +* { box-sizing: border-box; } +html, body { margin: 0; min-height: 100%; } +body { + font-family: "IBM Plex Sans", "Avenir Next", "Segoe UI", sans-serif; + background: + radial-gradient(circle at top left, rgba(94, 234, 212, 0.15), transparent 28%), + radial-gradient(circle at top right, rgba(255, 184, 107, 0.18), transparent 22%), + linear-gradient(145deg, #050c16 0%, #08111f 44%, #0d1c31 100%); + color: var(--text); + overflow-x: hidden; +} + +body::before { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + background-image: + linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px), + linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px); + background-size: 32px 32px; + mask-image: radial-gradient(circle at center, black 36%, transparent 78%); + opacity: 0.28; +} + +a { color: var(--accent); text-decoration: none; } +a:hover { text-decoration: underline; } +button, input, select { + font: inherit; +} + +.dashboard-shell { + position: relative; + max-width: 1640px; + margin: 0 auto; + padding: 32px 24px 40px; +} + +.hero { + position: sticky; + top: 0; + z-index: 20; + backdrop-filter: blur(18px); + background: linear-gradient(180deg, rgba(7, 17, 31, 0.94), rgba(7, 17, 31, 0.75)); + border: 1px solid var(--border); + border-radius: 28px; + padding: 24px 24px 20px; + box-shadow: var(--shadow); +} + +.hero-grid { + display: grid; + grid-template-columns: 1.6fr 1fr; + gap: 20px; + align-items: start; +} + +.eyebrow { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--muted); + font-size: 0.88rem; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.eyebrow::before { + content: ""; + width: 10px; + height: 10px; + border-radius: 999px; + background: radial-gradient(circle, var(--accent), rgba(94, 234, 212, 0.15)); + box-shadow: 0 0 24px rgba(94, 234, 212, 0.8); + animation: pulse 2.8s ease-in-out infinite; +} + +.hero h1 { + margin: 12px 0 10px; + font-family: "IBM Plex Serif", "Iowan Old Style", Georgia, serif; + font-size: clamp(2rem, 4vw, 3.5rem); + line-height: 1.02; +} + +.hero p { + margin: 0; + color: var(--muted); + max-width: 74ch; +} + +.hero-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 18px; +} + +.chip, .ghost-chip { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + border-radius: 999px; + border: 1px solid var(--border); + padding: 10px 14px; + background: rgba(255,255,255,0.06); + color: var(--text); +} + +.ghost-chip { + background: rgba(255,255,255,0.04); +} + +.hero-meta { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; +} + +.meta-card, .glass-panel { + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow); +} + +.meta-card { + padding: 18px; + min-height: 116px; +} + +.meta-card strong { + display: block; + color: var(--muted); + font-size: 0.84rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.meta-card span { + display: block; + margin-top: 10px; + font-size: 2rem; + font-weight: 700; +} + +.workspace { + display: grid; + grid-template-columns: 420px minmax(0, 1fr); + gap: 20px; + margin-top: 22px; +} + +.sidebar { + display: flex; + flex-direction: column; + gap: 18px; +} + +.panel-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 16px; +} + +.panel-header h2, .panel-header h3 { + margin: 0; + font-size: 1rem; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--muted); +} + +.glass-panel { + padding: 18px; + background: + linear-gradient(180deg, rgba(255,255,255,0.04), transparent 35%), + var(--panel); +} + +.filters { + display: grid; + gap: 12px; +} + +.filters label { + display: grid; + gap: 6px; + color: var(--muted); + font-size: 0.9rem; +} + +.filters input, .filters select { + width: 100%; + background: rgba(255,255,255,0.05); + color: var(--text); + border: 1px solid rgba(159, 179, 202, 0.18); + border-radius: 14px; + padding: 12px 14px; +} + +.run-list { + display: grid; + gap: 12px; + max-height: calc(100vh - 460px); + overflow: auto; + padding-right: 4px; +} + +.run-card { + width: 100%; + text-align: left; + padding: 16px; + border-radius: 18px; + border: 1px solid rgba(159, 179, 202, 0.14); + background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0.03)); + color: var(--text); + cursor: pointer; + transition: transform 180ms ease, border-color 180ms ease, background 180ms ease; +} + +.run-card:hover, .run-card.is-active { + transform: translateY(-1px); + border-color: rgba(94, 234, 212, 0.42); + background: linear-gradient(180deg, rgba(94, 234, 212, 0.14), rgba(255,255,255,0.05)); +} + +.run-card-top, .flex-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.run-card h4 { + margin: 10px 0 8px; + font-size: 1rem; + line-height: 1.35; +} + +.mini-muted { + color: var(--muted); + font-size: 0.86rem; +} + +.status-pill { + display: inline-flex; + align-items: center; + gap: 7px; + border-radius: 999px; + padding: 6px 10px; + font-size: 0.82rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + border: 1px solid transparent; +} + +.status-pill::before { + content: ""; + width: 8px; + height: 8px; + border-radius: 999px; + background: currentColor; + box-shadow: 0 0 16px currentColor; +} + +.status-blocked-artifact, .status-blocked-destructive { + color: var(--danger); + background: rgba(255, 123, 123, 0.14); + border-color: rgba(255, 123, 123, 0.24); +} + +.status-triage-manual, .status-suspected { + color: var(--warning); + background: rgba(255, 209, 102, 0.14); + border-color: rgba(255, 209, 102, 0.24); +} + +.status-verified-real { + color: var(--success); + background: rgba(110, 231, 165, 0.14); + border-color: rgba(110, 231, 165, 0.24); +} + +.status-verified-synthetic { + color: var(--accent-3); + background: rgba(144, 205, 244, 0.14); + border-color: rgba(144, 205, 244, 0.24); +} + +.status-default { + color: var(--accent); + background: rgba(94, 234, 212, 0.14); + border-color: rgba(94, 234, 212, 0.24); +} + +.detail-view { + display: grid; + gap: 18px; +} + +.detail-hero { + padding: 22px; + overflow: hidden; + position: relative; +} + +.detail-hero::after { + content: ""; + position: absolute; + inset: auto -20% -55% 25%; + height: 220px; + background: radial-gradient(circle, rgba(94, 234, 212, 0.2), transparent 55%); + pointer-events: none; +} + +.detail-headline { + margin: 8px 0 12px; + font-family: "IBM Plex Serif", "Iowan Old Style", Georgia, serif; + font-size: clamp(1.6rem, 3vw, 2.8rem); + line-height: 1.08; +} + +.tag-row, .link-row, .artifact-row { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.tag { + display: inline-flex; + align-items: center; + padding: 7px 10px; + border-radius: 999px; + background: rgba(255,255,255,0.06); + border: 1px solid rgba(159, 179, 202, 0.18); + color: var(--text); + font-size: 0.86rem; +} + +.stat-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; + margin-top: 18px; +} + +.stat-card { + padding: 14px; + border-radius: 16px; + background: rgba(255,255,255,0.04); + border: 1px solid rgba(159, 179, 202, 0.16); +} + +.stat-card strong { + display: block; + color: var(--muted); + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.stat-card span { + display: block; + margin-top: 10px; + font-size: 1.15rem; + font-weight: 700; +} + +.detail-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) 360px; + gap: 18px; +} + +.stack { + display: grid; + gap: 18px; +} + +.progress-strip { + display: grid; + gap: 12px; + margin-bottom: 16px; +} + +.progress-bar { + display: flex; + width: 100%; + min-height: 12px; + overflow: hidden; + border-radius: 999px; + background: rgba(255,255,255,0.08); + border: 1px solid rgba(159, 179, 202, 0.14); +} + +.progress-segment { + min-width: 10px; + transition: width 180ms ease; +} + +.progress-completed { background: linear-gradient(90deg, rgba(110, 231, 165, 0.9), rgba(94, 234, 212, 0.9)); } +.progress-blocked { background: linear-gradient(90deg, rgba(255, 123, 123, 0.95), rgba(255, 160, 122, 0.9)); } +.progress-failed { background: linear-gradient(90deg, rgba(255, 123, 123, 0.92), rgba(255, 209, 102, 0.88)); } +.progress-skipped { background: linear-gradient(90deg, rgba(255,255,255,0.22), rgba(159, 179, 202, 0.3)); } +.progress-planned { background: linear-gradient(90deg, rgba(144, 205, 244, 0.82), rgba(94, 234, 212, 0.72)); } +.progress-other { background: linear-gradient(90deg, rgba(255,255,255,0.18), rgba(255,255,255,0.1)); } + +.progress-legend { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.progress-legend .tag { + gap: 7px; +} + +.progress-legend .swatch { + width: 10px; + height: 10px; + border-radius: 999px; + display: inline-block; +} + +.stage-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 12px; + margin-bottom: 18px; +} + +.stage-card { + padding: 14px; + border-radius: 16px; + background: rgba(255,255,255,0.04); + border: 1px solid rgba(159, 179, 202, 0.16); +} + +.stage-card strong { + display: block; + margin-bottom: 10px; +} + +.accordion { + overflow: hidden; +} + +.accordion > summary { + list-style: none; + cursor: pointer; + padding: 18px 20px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.accordion > summary::-webkit-details-marker { display: none; } +.accordion > summary span { + font-size: 1rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--muted); +} + +.accordion .accordion-content { + padding: 0 20px 20px; + border-top: 1px solid rgba(159, 179, 202, 0.12); +} + +.timeline-list { + display: grid; + gap: 12px; +} + +.timeline-item { + display: grid; + grid-template-columns: 120px 180px minmax(0, 1fr); + gap: 12px; + padding: 12px 0; + border-bottom: 1px solid rgba(159, 179, 202, 0.12); +} + +.timeline-item:last-child { + border-bottom: 0; +} + +.timeline-step { + font-weight: 700; +} + +.artifact-group { + margin-bottom: 14px; +} + +.artifact-group h4 { + margin: 0 0 10px; + color: var(--muted); + font-size: 0.88rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.artifact-button { + display: inline-flex; + align-items: center; + gap: 8px; + margin: 0 10px 10px 0; + padding: 10px 12px; + border-radius: 14px; + border: 1px solid rgba(159, 179, 202, 0.16); + background: rgba(255,255,255,0.05); + color: var(--text); + cursor: pointer; +} + +.artifact-button:hover, .artifact-button.is-active { + border-color: rgba(94, 234, 212, 0.4); + background: rgba(94, 234, 212, 0.12); +} + +.log-viewer { + min-height: 420px; + display: grid; + gap: 14px; +} + +.viewer-toolbar { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 10px; + align-items: center; +} + +.viewer-frame { + background: rgba(2, 8, 22, 0.88); + border: 1px solid rgba(159, 179, 202, 0.18); + border-radius: 16px; + min-height: 300px; + overflow: hidden; +} + +.viewer-frame pre { + margin: 0; + padding: 18px; + max-height: 560px; + overflow: auto; + font-family: "IBM Plex Mono", "SFMono-Regular", "Menlo", monospace; + font-size: 0.88rem; + line-height: 1.6; + color: #d6e5f5; + white-space: pre-wrap; +} + +.viewer-frame img { + display: block; + width: 100%; + height: auto; +} + +.gallery { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 14px; +} + +.gallery button { + all: unset; + cursor: pointer; + border-radius: 18px; + overflow: hidden; + border: 1px solid rgba(159, 179, 202, 0.18); + background: rgba(255,255,255,0.04); +} + +.gallery img { + display: block; + width: 100%; + aspect-ratio: 4 / 3; + object-fit: cover; +} + +.gallery figcaption { + padding: 10px 12px 14px; + color: var(--muted); + font-size: 0.84rem; +} + +.failure-callout { + padding: 16px 18px; + border-radius: 18px; + border: 1px solid rgba(255, 123, 123, 0.2); + background: rgba(255, 123, 123, 0.09); +} + +.json-block { + background: rgba(2, 8, 22, 0.72); + border-radius: 16px; + border: 1px solid rgba(159, 179, 202, 0.14); + padding: 16px; + overflow: auto; + font-family: "IBM Plex Mono", "SFMono-Regular", monospace; + font-size: 0.84rem; + line-height: 1.55; + color: #c9d8e8; +} + +.empty-state { + padding: 40px 24px; + text-align: center; + color: var(--muted); +} + +.failure-feed { + display: grid; + gap: 10px; +} + +.failure-item { + padding: 12px 14px; + border-radius: 16px; + background: rgba(255,255,255,0.04); + border: 1px solid rgba(159, 179, 202, 0.16); +} + +.system-grid { + display: grid; + gap: 10px; +} + +.system-card { + padding: 14px 16px; + border-radius: 16px; + background: rgba(255,255,255,0.04); + border: 1px solid rgba(159, 179, 202, 0.14); +} + +.meter { + position: relative; + height: 10px; + border-radius: 999px; + background: rgba(255,255,255,0.08); + overflow: hidden; + margin-top: 10px; +} + +.meter > span { + position: absolute; + inset: 0 auto 0 0; + width: var(--fill, 0%); + background: linear-gradient(90deg, var(--accent), var(--accent-2)); + border-radius: inherit; +} + +.sync-indicator { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.sync-indicator strong { + color: var(--text); +} + +.dot { + width: 10px; + height: 10px; + border-radius: 999px; + background: var(--accent); + box-shadow: 0 0 18px rgba(94, 234, 212, 0.8); +} + +@keyframes pulse { + 0%, 100% { transform: scale(1); opacity: 0.88; } + 50% { transform: scale(1.35); opacity: 1; } +} + +@media (max-width: 1280px) { + .workspace, .detail-grid, .hero-grid { + grid-template-columns: 1fr; + } + + .stat-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 760px) { + .dashboard-shell { + padding: 18px 14px 32px; + } + + .hero { + position: static; + } + + .stat-grid, .hero-meta { + grid-template-columns: 1fr; + } + + .timeline-item { + grid-template-columns: 1fr; + } +} diff --git a/08-threat-intel/generated/dashboard/legacy/docs b/08-threat-intel/generated/dashboard/legacy/docs new file mode 120000 index 00000000..a9594bfe --- /dev/null +++ b/08-threat-intel/generated/dashboard/legacy/docs @@ -0,0 +1 @@ +../docs \ No newline at end of file diff --git a/08-threat-intel/generated/dashboard/legacy/index.html b/08-threat-intel/generated/dashboard/legacy/index.html new file mode 100644 index 00000000..3bbf6390 --- /dev/null +++ b/08-threat-intel/generated/dashboard/legacy/index.html @@ -0,0 +1,81 @@ + + + + + + + websafe authorized lab dashboard + + + +
+
+
+
+
Authorized Lab Dashboard
+

本地攻防实证工作台

+

面向授权实验场景的本地静态前端。聚合 advisory、run bundle、日志、浏览器证据、失败原因、利用思路与源头信息,并支持可折叠细节与自动刷新。

+
+ + + Open Summary JSON + Open Feature Docs +
+
+
+
+

Sync State

+
BootingLoading generated JSON
+
+
+
+
+
+ +
+ + +
+
Select a run to inspect full details.
+
+
+
+ + + diff --git a/08-threat-intel/generated/dashboard/legacy/profiles.json b/08-threat-intel/generated/dashboard/legacy/profiles.json new file mode 120000 index 00000000..1263bb32 --- /dev/null +++ b/08-threat-intel/generated/dashboard/legacy/profiles.json @@ -0,0 +1 @@ +../profiles.json \ No newline at end of file diff --git a/08-threat-intel/generated/dashboard/legacy/runs b/08-threat-intel/generated/dashboard/legacy/runs new file mode 120000 index 00000000..75e26c30 --- /dev/null +++ b/08-threat-intel/generated/dashboard/legacy/runs @@ -0,0 +1 @@ +../runs \ No newline at end of file diff --git a/08-threat-intel/generated/dashboard/legacy/runs.json b/08-threat-intel/generated/dashboard/legacy/runs.json new file mode 120000 index 00000000..01c7dd2a --- /dev/null +++ b/08-threat-intel/generated/dashboard/legacy/runs.json @@ -0,0 +1 @@ +../runs.json \ No newline at end of file diff --git a/08-threat-intel/generated/dashboard/legacy/summary.json b/08-threat-intel/generated/dashboard/legacy/summary.json new file mode 120000 index 00000000..2e8e8a42 --- /dev/null +++ b/08-threat-intel/generated/dashboard/legacy/summary.json @@ -0,0 +1 @@ +../summary.json \ No newline at end of file diff --git a/08-threat-intel/generated/dashboard/legacy/systems.json b/08-threat-intel/generated/dashboard/legacy/systems.json new file mode 120000 index 00000000..0640cf27 --- /dev/null +++ b/08-threat-intel/generated/dashboard/legacy/systems.json @@ -0,0 +1 @@ +../systems.json \ No newline at end of file diff --git a/08-threat-intel/generated/dashboard/summary.json b/08-threat-intel/generated/dashboard/summary.json index a2eccfaa..e029f8c2 100644 --- a/08-threat-intel/generated/dashboard/summary.json +++ b/08-threat-intel/generated/dashboard/summary.json @@ -1,5 +1,5 @@ { - "generated_at": "2026-03-17T07:36:43+00:00", + "generated_at": "2026-03-17T08:20:30+00:00", "advisory_count": 89, "run_count": 3, "statuses": { diff --git a/08-threat-intel/generated/latest-ingest.md b/08-threat-intel/generated/latest-ingest.md index 7747faf4..38790504 100644 --- a/08-threat-intel/generated/latest-ingest.md +++ b/08-threat-intel/generated/latest-ingest.md @@ -1,6 +1,6 @@ # 最新同步摘要 -- 渲染时间: `2026-03-17T07:36:11+00:00` +- 渲染时间: `2026-03-17T08:20:30+00:00` - 系统数量: `62` - Advisory 数量: `89` - 重点 Markdown 数量: `89` diff --git a/08-threat-intel/generated/run-summary.json b/08-threat-intel/generated/run-summary.json index 677f2cd9..3c1f1250 100644 --- a/08-threat-intel/generated/run-summary.json +++ b/08-threat-intel/generated/run-summary.json @@ -1,5 +1,5 @@ { - "generated_at": "2026-03-17T07:36:11+00:00", + "generated_at": "2026-03-17T08:20:30+00:00", "system_count": 62, "advisory_count": 89, "markdown_count": 89, diff --git a/README.md b/README.md index 8cd194cb..8174ddd2 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,15 @@ python3 /Users/x/websafe/scripts/lab/main.py run-batch --only-hotlane --limit 10 python3 /Users/x/websafe/scripts/lab/main.py serve-dashboard --port 8734 ``` +本地 dashboard 路由: + +- `/index.html` + - 默认正式 UI,使用本地化 Lovart 视觉壳层 +- `/legacy/index.html` + - 旧版工作台回退入口 +- `/docs/design-source.html` + - 设计来源与本地化说明 + 计划中的本机 cron 入口: - [run-hourly.sh](/Users/x/websafe/scripts/intel/run-hourly.sh) 处理 KEV / 在野利用 / 极高优先级更新,并触发 hotlane 实证队列 diff --git a/docs/frontend-dashboard-design.md b/docs/frontend-dashboard-design.md index ca37a9b6..e036f9d6 100644 --- a/docs/frontend-dashboard-design.md +++ b/docs/frontend-dashboard-design.md @@ -31,6 +31,8 @@ - 信息密度高,但必须可折叠、可筛选、可逐层展开 - 日志与原始 JSON 必须能直接预览 - 页面视觉应更生动,但不能牺牲扫描效率 +- 默认路由采用正式新 UI,同时保留 `legacy` 回退入口 +- 运行期不得依赖外部 HTML、字体 CDN 或图标 CDN ## 3. 信息架构 @@ -288,7 +290,22 @@ flowchart LR 前端不直接写这些数据,只读取并展示。 -## 10. 落地约束 +## 10. 路由与文档地址 + +- `/index.html` + - 默认正式入口,使用本地化 Lovart UI 外壳 +- `/legacy/index.html` + - 旧版 dashboard 回退入口 +- `/docs/project-features.html` + - 功能说明镜像页 +- `/docs/frontend-dashboard-design.html` + - 设计说明镜像页 +- `/docs/secure-code-index.html` + - secure-code 索引镜像页 +- `/docs/design-source.html` + - Lovart vendor 来源和本地化说明 + +## 11. 落地约束 - 保持静态前端,不引入长期运行后端 - 本地 `serve-dashboard` 即可查看 @@ -296,7 +313,7 @@ flowchart LR - 不依赖第三方 CDN UI 库 - 优先使用原生 HTML/CSS/JS,可长期维护 -## 11. 验收标准 +## 12. 验收标准 页面完成后,应满足: diff --git a/docs/project-features.md b/docs/project-features.md index 78f2ea52..1655b67c 100644 --- a/docs/project-features.md +++ b/docs/project-features.md @@ -61,6 +61,9 @@ - `report.md`, `report.html`, `timeline.mmd`, `assets/`, `logs/` - `08-threat-intel/generated/dashboard/` - 静态前端工作台 + - `/index.html` 为本地化 Lovart 正式 UI + - `/legacy/index.html` 为旧版工作台回退入口 + - `/docs/*.html` 为本地可访问的说明与设计镜像页 - `07-framework-security/` - 系统级 README、INDEX、案例页,自动显示本地实证状态 @@ -160,3 +163,12 @@ python3 /Users/x/websafe/scripts/lab/main.py serve-dashboard --port 8734 详细设计见: - [本地前端工作台设计文档](/Users/x/websafe/docs/frontend-dashboard-design.md) + +当前地址布局固定为: + +- `/index.html` + - 默认新 UI,基于本地化 Lovart 视觉壳层,绑定真实 dashboard JSON +- `/legacy/index.html` + - 旧版工作台显式保留,用于快速回退和对照 +- `/docs/design-source.html` + - 设计来源、vendor manifest 与本地化说明 diff --git a/scripts/intel/validators.py b/scripts/intel/validators.py index 2dff384f..6451699a 100644 --- a/scripts/intel/validators.py +++ b/scripts/intel/validators.py @@ -34,6 +34,14 @@ REQUIRED_SYSTEM_FIELDS = { "render_policy", } +FORBIDDEN_RUNTIME_PATTERNS = [ + "assets-persist.lovart.ai", + "cdnjs.cloudflare.com", + "remixicon", + "fonts.googleapis.com", + "fonts.gstatic.com", +] + def validate(source_map: Dict[str, Any]) -> List[str]: errors: List[str] = [] @@ -82,6 +90,7 @@ def validate(source_map: Dict[str, Any]) -> List[str]: GENERATED_DIR / "latest-ingest.md", GENERATED_DIR / "run-summary.json", GENERATED_DIR / "dashboard" / "index.html", + GENERATED_DIR / "dashboard" / "legacy" / "index.html", GENERATED_DIR / "dashboard" / "summary.json", GENERATED_DIR / "dashboard" / "systems.json", GENERATED_DIR / "dashboard" / "runs.json", @@ -89,14 +98,35 @@ def validate(source_map: Dict[str, Any]) -> List[str]: GENERATED_DIR / "dashboard" / "profiles.json", GENERATED_DIR / "dashboard" / "assets" / "app.js", GENERATED_DIR / "dashboard" / "assets" / "styles.css", + GENERATED_DIR / "dashboard" / "assets" / "icons.svg", + GENERATED_DIR / "dashboard" / "assets" / "design-source.json", + GENERATED_DIR / "dashboard" / "legacy" / "assets" / "app.js", + GENERATED_DIR / "dashboard" / "legacy" / "assets" / "styles.css", GENERATED_DIR / "dashboard" / "docs" / "project-features.html", GENERATED_DIR / "dashboard" / "docs" / "frontend-dashboard-design.html", GENERATED_DIR / "dashboard" / "docs" / "secure-code-index.html", + GENERATED_DIR / "dashboard" / "docs" / "design-source.html", ROOT / "08-threat-intel" / "registry" / "source-confidence.md", ]: if not path.exists(): errors.append(f"generated artifact missing: {path}") + runtime_files = [ + GENERATED_DIR / "dashboard" / "index.html", + GENERATED_DIR / "dashboard" / "assets" / "app.js", + GENERATED_DIR / "dashboard" / "assets" / "styles.css", + GENERATED_DIR / "dashboard" / "legacy" / "index.html", + GENERATED_DIR / "dashboard" / "legacy" / "assets" / "app.js", + GENERATED_DIR / "dashboard" / "legacy" / "assets" / "styles.css", + ] + for runtime_file in runtime_files: + if not runtime_file.exists(): + continue + content = runtime_file.read_text(encoding="utf-8") + for pattern in FORBIDDEN_RUNTIME_PATTERNS: + if pattern in content: + errors.append(f"forbidden runtime dependency in {runtime_file}: {pattern}") + if not (SECURE_CODE_ROOT / "README.md").exists(): errors.append(f"secure-code README missing: {SECURE_CODE_ROOT / 'README.md'}") diff --git a/scripts/lab/dashboard_templates/lovart/assets/app.js b/scripts/lab/dashboard_templates/lovart/assets/app.js new file mode 100644 index 00000000..519437b9 --- /dev/null +++ b/scripts/lab/dashboard_templates/lovart/assets/app.js @@ -0,0 +1,708 @@ +const state = { + summary: null, + runs: [], + systems: [], + advisories: {}, + profiles: {}, + selectedRunId: null, + selectedArtifact: null, + refreshHandle: null, + refreshMs: 5000, + autoRefresh: true, + filters: { + search: "", + system: "", + status: "", + profile: "" + }, + panels: { + timeline: true, + reasoning: true, + evidence: true, + logs: true, + sources: true, + run_json: false, + advisory_json: false, + profile_json: false + } +}; + +const $ = (id) => document.getElementById(id); +const icon = (name, className = "icon") => + ``; + +const statusClass = (status) => ({ + "verified-real": "status-pill status-verified-real", + "verified-synthetic": "status-pill status-verified-synthetic", + "blocked-artifact": "status-pill status-blocked-artifact", + "blocked-destructive": "status-pill status-blocked-destructive", + "triage-manual": "status-pill status-triage-manual", + "suspected": "status-pill status-suspected", + completed: "status-pill status-verified-real", + failed: "status-pill status-blocked-artifact", + skipped: "status-pill status-triage-manual" +}[status] || "status-pill status-default"); + +function escapeHtml(value) { + return String(value ?? "") + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """); +} + +function formatStatus(value) { + return String(value || "unknown").replaceAll("-", " "); +} + +function timeAgo(value) { + if (!value) return "-"; + const diff = Date.now() - new Date(value).getTime(); + if (Number.isNaN(diff)) return value; + const seconds = Math.floor(diff / 1000); + if (seconds < 60) return `${seconds}s ago`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; +} + +async function fetchJson(url) { + const response = await fetch(`${url}?t=${Date.now()}`, { cache: "no-store" }); + if (!response.ok) { + throw new Error(`${url} -> ${response.status}`); + } + return response.json(); +} + +function distinct(values) { + return [...new Set(values.filter(Boolean))].sort(); +} + +function sumStatuses(predicate) { + return Object.entries(state.summary?.statuses || {}) + .filter(([key]) => predicate(key)) + .reduce((sum, [, value]) => sum + Number(value || 0), 0); +} + +function metricCards() { + const successCount = Number(state.summary?.statuses?.["verified-real"] || 0) + Number(state.summary?.statuses?.["verified-synthetic"] || 0); + const blockedCount = sumStatuses((key) => key.startsWith("blocked")); + const inProgressCount = Math.max(Number(state.summary?.run_count || 0) - successCount - blockedCount, 0); + + return [ + { + label: "Total Runs", + value: state.summary?.run_count || 0, + note: `${state.summary?.advisory_count || 0} advisories indexed`, + color: "var(--accent-purple)", + iconName: "report" + }, + { + label: "Success", + value: successCount, + note: "verified-real + verified-synthetic", + color: "var(--accent-green)", + iconName: "shield" + }, + { + label: "Blocked", + value: blockedCount, + note: "artifact or destructive blockers", + color: "var(--accent-red)", + iconName: "failure" + }, + { + label: "In Progress", + value: inProgressCount, + note: "manual review or incomplete verification", + color: "var(--accent-blue)", + iconName: "timeline" + } + ]; +} + +function renderMetrics() { + $("metricCards").innerHTML = metricCards() + .map( + (card) => ` +
+
${icon(card.iconName)}${escapeHtml(card.label)}
+
${escapeHtml(card.value)}
+
${escapeHtml(card.note)}
+
+ ` + ) + .join(""); +} + +function renderSyncState(kind, title, detail) { + $("syncState").innerHTML = ` + ${icon("sync", "icon icon-sync")} +
+ ${escapeHtml(title)} + ${escapeHtml(detail)} +
+ `; + $("syncState").dataset.kind = kind; +} + +function hydrateFilters() { + const controls = [ + ["systemFilter", "system", state.runs.map((item) => item.system_id), "All systems"], + ["statusFilter", "status", state.runs.map((item) => item.verification_status), "All statuses"], + ["profileFilter", "profile", state.runs.map((item) => item.repro_profile_id), "All profiles"] + ]; + + for (const [id, key, values, label] of controls) { + const control = $(id); + const current = state.filters[key]; + control.innerHTML = ``; + control.innerHTML += distinct(values) + .map((value) => ``) + .join(""); + control.value = current; + } +} + +function filteredRuns() { + return state.runs.filter((item) => { + if (state.filters.system && item.system_id !== state.filters.system) return false; + if (state.filters.status && item.verification_status !== state.filters.status) return false; + if (state.filters.profile && item.repro_profile_id !== state.filters.profile) return false; + if (!state.filters.search) return true; + const haystack = [ + item.run_id, + item.advisory_id, + item.system_id, + item.repro_profile_id, + item.advisory_meta?.title || "", + item.advisory_meta?.summary || "" + ] + .join(" ") + .toLowerCase(); + return haystack.includes(state.filters.search); + }); +} + +function renderSystems() { + $("systemStats").innerHTML = state.systems.length + ? state.systems + .map((system) => { + const total = Math.max(Number(system.total || 0), 1); + const verified = Number(system.verified_real || 0) + Number(system.verified_synthetic || 0); + const coverage = Math.round((verified / total) * 100); + return ` +
+
+ ${escapeHtml(system.display_name || system.system_id)} + ${escapeHtml(system.browser_present || 0)}/${escapeHtml(system.browser_required || 0)} browser +
+
${escapeHtml(system.system_id)} · latest ${escapeHtml(system.latest_update || "-")}
+
+ real ${escapeHtml(system.verified_real || 0)} + synthetic ${escapeHtml(system.verified_synthetic || 0)} + blocked ${escapeHtml(system.blocked || 0)} +
+
+
+ `; + }) + .join("") + : `
No system coverage data.
`; +} + +function renderRecentFailures() { + const failures = state.summary?.recent_failures || []; + $("recentFailures").innerHTML = failures.length + ? failures + .map( + (item) => ` +
+
+ ${escapeHtml(item.run_id)} + ${escapeHtml(formatStatus(item.status))} +
+
${escapeHtml(item.title || item.advisory_id)}
+
${escapeHtml(item.blocked_reason || "-")}
+
+ ` + ) + .join("") + : `
No recent blockers.
`; +} + +function renderRunQueue() { + const runs = filteredRuns(); + $("runCount").textContent = `${runs.length} shown`; + $("runQueue").innerHTML = runs.length + ? runs + .map((item) => { + const active = item.run_id === state.selectedRunId ? "is-active" : ""; + const browserState = item.browser_evidence?.present ? "ready" : (item.browser_evidence?.required ? "required" : "optional"); + const lead = item.reasoning_lines?.[0] || item.blocked_reason || item.advisory_meta?.summary || ""; + return ` + + `; + }) + .join("") + : `
No runs match the current filters.
`; + + document.querySelectorAll("[data-run-id]").forEach((button) => { + button.addEventListener("click", () => { + state.selectedRunId = button.dataset.runId; + location.hash = `run=${state.selectedRunId}`; + renderRunQueue(); + renderDetail(); + }); + }); +} + +function progressSegments(progress) { + const order = [ + ["completed", "Completed", "progress-completed"], + ["blocked", "Blocked", "progress-blocked"], + ["failed", "Failed", "progress-failed"], + ["skipped", "Skipped", "progress-skipped"], + ["planned", "Planned", "progress-planned"], + ["other", "Other", "progress-other"] + ]; + const total = order.reduce((sum, [key]) => sum + Number(progress?.[key] || 0), 0); + if (!total) { + return { + bar: `
`, + legend: `no progress` + }; + } + const bar = order + .filter(([key]) => Number(progress?.[key] || 0) > 0) + .map(([key, _label, className]) => { + const pct = Math.max((Number(progress[key] || 0) / total) * 100, 4); + return ``; + }) + .join(""); + const legend = order + .filter(([key]) => Number(progress?.[key] || 0) > 0) + .map(([key, label, className]) => `${escapeHtml(label)} ${escapeHtml(progress[key] || 0)}`) + .join(""); + return { bar, legend }; +} + +function timelineTone(status) { + if (status === "completed" || status === "verified-real" || status === "verified-synthetic") return "timeline-success"; + if (String(status || "").startsWith("blocked") || status === "failed") return "timeline-blocked"; + if (status === "planned") return "timeline-pending"; + return "timeline-neutral"; +} + +function renderPanel(panelKey, title, meta, iconName, content) { + const open = state.panels[panelKey] !== false; + return ` +
+ +
+
${content}
+
+
+ `; +} + +function defaultArtifact(run) { + const preference = ["attack", "requests", "container", "browser", "baseline", "compose", "reports"]; + for (const key of preference) { + const group = (run.artifact_groups || []).find((item) => item.key === key && item.items?.length); + if (!group) continue; + const textItem = group.items.find((item) => item.kind === "text"); + return textItem || group.items[0]; + } + return null; +} + +async function openArtifact(href, label, kind) { + state.selectedArtifact = { href, label, kind }; + document.querySelectorAll(".artifact-button").forEach((button) => { + button.classList.toggle("is-active", button.dataset.href === href); + }); + + const labelNode = $("viewerLabel"); + const metaNode = $("viewerMeta"); + const openNode = $("viewerOpen"); + const viewer = $("viewerFrame"); + if (!labelNode || !metaNode || !openNode || !viewer) return; + + labelNode.textContent = label; + metaNode.textContent = href; + openNode.href = href; + + try { + if (kind === "image") { + viewer.innerHTML = `${escapeHtml(label)}`; + return; + } + if (href.endsWith(".html")) { + viewer.innerHTML = ``; + return; + } + const response = await fetch(`${href}?t=${Date.now()}`, { cache: "no-store" }); + if (!response.ok) throw new Error(`${href} -> ${response.status}`); + const text = await response.text(); + let formatted = text; + if (href.endsWith(".json")) { + try { + formatted = JSON.stringify(JSON.parse(text), null, 2); + } catch (_error) { + } + } + viewer.innerHTML = `
${escapeHtml(formatted)}
`; + } catch (error) { + viewer.innerHTML = `
Artifact load failed: ${escapeHtml(error.message)}
`; + } +} + +function bindPanelToggles() { + document.querySelectorAll("[data-panel-toggle]").forEach((button) => { + button.addEventListener("click", () => { + const key = button.dataset.panelToggle; + state.panels[key] = !(state.panels[key] !== false); + const panel = document.querySelector(`[data-panel="${key}"]`); + if (panel) { + panel.classList.toggle("is-collapsed", state.panels[key] === false); + } + }); + }); +} + +function renderDetail() { + const run = state.runs.find((item) => item.run_id === state.selectedRunId); + if (!run) { + $("detailWorkspace").innerHTML = ` +
+ ${icon("shield", "icon icon-xl")} +

Select a run

+

Pick a run from the left queue to inspect timeline, evidence, logs and raw JSON.

+
+ `; + return; + } + + const advisory = run.advisory_meta || {}; + const profile = run.profile_meta || {}; + const screenshotItems = ((run.artifact_groups || []).find((group) => group.key === "browser")?.items || []).filter((item) => item.kind === "image"); + const segments = progressSegments(run.progress || {}); + const browserStatus = run.browser_evidence?.present ? "Ready" : (run.browser_evidence?.required ? "Required" : "Optional"); + const artifactCount = (run.artifact_groups || []).reduce((sum, group) => sum + Number(group.count || 0), 0); + + const timelineContent = ` +
${segments.bar}
+
${segments.legend}
+
+ ${(run.timeline || []) + .map((item) => ` +
+ +
+ ${escapeHtml(item.step || "-")} + ${escapeHtml(item.at || "-")} +
+
${escapeHtml(formatStatus(item.status || "unknown"))}
+
${escapeHtml(item.detail || "-")}
+
+ `) + .join("") || `
No timeline items recorded.
`} +
+ `; + + const reasoningCards = [ + { + label: "Summary", + copy: advisory.summary || "No advisory summary available." + }, + { + label: "Success Criteria", + copy: (profile.success_criteria || []).join(" | ") || "No success criteria defined." + }, + { + label: "Seed / Attack Notes", + copy: (run.reasoning_lines || []).join("\n\n") || "No reasoning lines recorded." + }, + { + label: "Allowed Targets", + copy: (profile.allowed_target_types || []).join(", ") || "No target scope declared." + } + ]; + + const reasoningContent = ` + ${run.blocked_reason ? `
Failure reason
${escapeHtml(run.blocked_reason)}
` : ""} +
+ vuln family ${escapeHtml(profile.vuln_family || "unknown")} + cleanup ${escapeHtml(profile.cleanup_policy || "-")} + destructive risk ${escapeHtml(profile.destructive_risk || "-")} + artifact ${escapeHtml(run.artifact_mode || "-")} +
+
+ ${reasoningCards + .map( + (card) => ` +
+ ${escapeHtml(card.label)} +
${escapeHtml(card.copy)}
+
+ ` + ) + .join("")} +
+ `; + + const evidenceContent = ` +
+ ${(run.artifact_groups || []) + .map( + (group) => ` +
+

${escapeHtml(group.label)} · ${escapeHtml(group.count)}

+
+ ${group.items + .map( + (item) => ` + + ` + ) + .join("")} +
+
+ ` + ) + .join("") || `
No artifact groups for this run.
`} + + ${ + screenshotItems.length + ? `` + : "" + } +
+ `; + + const logContent = ` +
+
+
+
${escapeHtml(state.selectedArtifact?.label || "Select an artifact")}
+
${escapeHtml(state.selectedArtifact?.href || "Artifact preview will appear here.")}
+
+
+ ${icon("link")}Open artifact + +
+
+
Select a report, log, screenshot, JSON or HTML artifact to preview it here.
+
+ `; + + const sourcesContent = ` +
+ ${(advisory.aliases || []).map((alias) => `${escapeHtml(alias)}`).join("")} + ${(advisory.secure_code_topics || []).map((topic) => `${escapeHtml(topic)}`).join("")} +
+ + `; + + const rawRunContent = `
${escapeHtml(JSON.stringify(run, null, 2))}
`; + const rawAdvisoryContent = `
${escapeHtml(JSON.stringify(advisory, null, 2))}
`; + const rawProfileContent = `
${escapeHtml(JSON.stringify(profile, null, 2))}
`; + + $("detailWorkspace").innerHTML = ` +
+
+ ${escapeHtml(formatStatus(run.verification_status))} +
+ ${escapeHtml(run.system_id)} + ${escapeHtml(run.repro_profile_id)} + ${escapeHtml(run.verification_mode || "-")} + ${escapeHtml(run.target_env || "-")} +
+
+

${escapeHtml(advisory.title || run.advisory_id)}

+
${escapeHtml(advisory.summary || "No advisory summary available.")}
+ + + +
+
+ Timeline Steps + ${escapeHtml(run.timeline?.length || 0)} +
+
+ Artifacts + ${escapeHtml(artifactCount)} +
+
+ Browser Evidence + ${escapeHtml(browserStatus)} +
+
+ Finished + ${escapeHtml(timeAgo(run.finished_at))} +
+
+
+ + ${renderPanel("timeline", "Progress Timeline", `${escapeHtml(run.timeline?.length || 0)} steps`, "timeline", timelineContent)} + ${renderPanel("reasoning", "Attack Plan & Reasoning", escapeHtml(profile.vuln_family || "unknown"), "reasoning", reasoningContent)} + ${renderPanel("evidence", "Evidence Explorer", `${escapeHtml(run.artifact_groups?.length || 0)} groups`, "evidence", evidenceContent)} + ${renderPanel("logs", "Live Log Viewer", state.selectedArtifact ? "active" : "idle", "logs", logContent)} + ${renderPanel("sources", "Sources & Fix Topics", `${escapeHtml((advisory.secondary_source_urls || []).length + (advisory.official_source_url ? 1 : 0))} links`, "sources", sourcesContent)} + ${renderPanel("run_json", "Run JSON", "raw", "json", rawRunContent)} + ${renderPanel("advisory_json", "Advisory JSON", "raw", "json", rawAdvisoryContent)} + ${renderPanel("profile_json", "Profile JSON", "raw", "json", rawProfileContent)} + `; + + bindPanelToggles(); + + document.querySelectorAll("[data-artifact]").forEach((button) => { + button.addEventListener("click", () => openArtifact(button.dataset.href, button.dataset.label, button.dataset.kind)); + }); + + $("viewerRefresh")?.addEventListener("click", () => { + if (state.selectedArtifact) { + openArtifact(state.selectedArtifact.href, state.selectedArtifact.label, state.selectedArtifact.kind); + } + }); + + const artifactExists = (run.artifact_groups || []).some((group) => group.items.some((item) => item.href === state.selectedArtifact?.href)); + const defaultItem = artifactExists ? state.selectedArtifact : defaultArtifact(run); + if (defaultItem) { + openArtifact(defaultItem.href, defaultItem.label, defaultItem.kind); + } +} + +function renderAll() { + renderMetrics(); + renderSystems(); + renderRecentFailures(); + renderRunQueue(); + renderDetail(); +} + +function attachGlobalEvents() { + $("searchInput").addEventListener("input", (event) => { + state.filters.search = String(event.target.value || "").trim().toLowerCase(); + renderRunQueue(); + }); + + [ + ["systemFilter", "system"], + ["statusFilter", "status"], + ["profileFilter", "profile"] + ].forEach(([id, key]) => { + $(id).addEventListener("input", (event) => { + state.filters[key] = String(event.target.value || ""); + renderRunQueue(); + }); + }); + + $("refreshDashboard").addEventListener("click", () => loadData(false)); + $("autoRefresh").addEventListener("change", (event) => { + state.autoRefresh = Boolean(event.target.checked); + startRefreshLoop(); + }); +} + +function startRefreshLoop() { + if (state.refreshHandle) { + clearInterval(state.refreshHandle); + state.refreshHandle = null; + } + if (!state.autoRefresh) return; + state.refreshHandle = setInterval(() => loadData(true), state.refreshMs); +} + +async function loadData(preserveSelection = true) { + const previous = state.selectedRunId; + renderSyncState("loading", "Refreshing", new Date().toLocaleTimeString()); + + try { + const [summary, runs, systems, advisories, profiles] = await Promise.all([ + fetchJson("./summary.json"), + fetchJson("./runs.json"), + fetchJson("./systems.json"), + fetchJson("./advisories.json"), + fetchJson("./profiles.json") + ]); + + state.summary = summary; + state.runs = runs; + state.systems = systems; + state.advisories = advisories; + state.profiles = profiles; + hydrateFilters(); + + const hashRun = location.hash.startsWith("#run=") ? location.hash.replace("#run=", "") : null; + const candidate = preserveSelection ? (hashRun || previous) : hashRun; + if (candidate && runs.some((item) => item.run_id === candidate)) { + state.selectedRunId = candidate; + } else { + state.selectedRunId = runs[0]?.run_id || null; + } + + renderAll(); + renderSyncState("live", "Live", summary.generated_at || new Date().toISOString()); + } catch (error) { + $("runQueue").innerHTML = `
Dashboard load failed: ${escapeHtml(error.message)}
`; + $("detailWorkspace").innerHTML = `

Load failed

${escapeHtml(error.message)}

`; + renderSyncState("error", "Load Failed", error.message); + } +} + +async function init() { + attachGlobalEvents(); + await loadData(false); + startRefreshLoop(); + window.addEventListener("hashchange", () => loadData(false)); +} + +document.addEventListener("DOMContentLoaded", init); diff --git a/scripts/lab/render.py b/scripts/lab/render.py index 206296b1..f20a06d6 100644 --- a/scripts/lab/render.py +++ b/scripts/lab/render.py @@ -1,7 +1,9 @@ from __future__ import annotations import html +import json import os +import shutil from pathlib import Path from typing import Any, Dict, List @@ -10,6 +12,13 @@ from lab.repro import load_profiles from lab.utils import ensure_dir, isoformat, load_json_dir, now_utc, unique, write_json, write_text +TEMPLATES_DIR = ROOT / "scripts" / "lab" / "dashboard_templates" +LOVART_TEMPLATE_DIR = TEMPLATES_DIR / "lovart" +LEGACY_TEMPLATE_DIR = TEMPLATES_DIR / "legacy" +LOVART_VENDOR_MANIFEST = LOVART_TEMPLATE_DIR / "vendor" / "source-manifest.json" +ROOT_JSON_FILES = ["summary.json", "runs.json", "systems.json", "advisories.json", "profiles.json"] + + def mermaid_from_steps(run: Dict[str, Any]) -> str: lines = [ "flowchart LR", @@ -52,11 +61,7 @@ def _artifact_kind(href: str) -> str: def _artifact_item(run: Dict[str, Any], href: str, label: str | None = None) -> Dict[str, Any]: - return { - "href": href, - "label": label or Path(href).name, - "kind": _artifact_kind(href), - } + return {"href": href, "label": label or Path(href).name, "kind": _artifact_kind(href)} def _artifact_group(run: Dict[str, Any], key: str, label: str, refs: List[str], use_dashboard_refs: bool = False) -> Dict[str, Any]: @@ -64,12 +69,7 @@ def _artifact_group(run: Dict[str, Any], key: str, label: str, refs: List[str], for ref in refs: href = ref if use_dashboard_refs else _dashboard_ref(run, ref) items.append(_artifact_item(run, href)) - return { - "key": key, - "label": label, - "count": len(items), - "items": items, - } + return {"key": key, "label": label, "count": len(items), "items": items} def _attack_result_refs(run: Dict[str, Any]) -> List[str]: @@ -157,9 +157,7 @@ def _reasoning_lines(advisory: Dict[str, Any], profile: Dict[str, Any]) -> List[ return unique(notes) -def _dashboard_doc_page(title: str, source_path: Path, description: str) -> str: - source_label = source_path.relative_to(ROOT) if source_path.is_relative_to(ROOT) else source_path - body = source_path.read_text(encoding="utf-8") if source_path.exists() else f"missing source: {source_path}" +def _dashboard_doc_page(title: str, body: str, description: str) -> str: return f""" @@ -247,7 +245,7 @@ def _dashboard_doc_page(title: str, source_path: Path, description: str) -> str: Back to dashboard

{html.escape(title)}

-
{html.escape(description)} | source: {html.escape(str(source_label))}
+
{html.escape(description)}
{html.escape(body)}
@@ -256,31 +254,109 @@ def _dashboard_doc_page(title: str, source_path: Path, description: str) -> str: """ +def _remove_path(path: Path) -> None: + if not path.exists() and not path.is_symlink(): + return + if path.is_symlink() or path.is_file(): + path.unlink() + return + shutil.rmtree(path) + + +def _sync_symlink(target: Path, link_path: Path) -> None: + ensure_dir(link_path.parent) + relative_target = os.path.relpath(target, link_path.parent) + if link_path.is_symlink() and os.readlink(link_path) == relative_target: + return + _remove_path(link_path) + os.symlink(relative_target, link_path, target_is_directory=target.is_dir()) + + +def _copy_tree(source: Path, destination: Path) -> None: + ensure_dir(destination) + for path in source.rglob("*"): + relative = path.relative_to(source) + target = destination / relative + if path.is_dir(): + ensure_dir(target) + continue + ensure_dir(target.parent) + shutil.copy2(path, target) + + def _write_dashboard_docs() -> None: docs_dir = DASHBOARD_DIR / "docs" ensure_dir(docs_dir) - docs = [ + sources = [ ( "project-features.html", "项目功能与特性总览", - ROOT / "docs" / "project-features.md", + (ROOT / "docs" / "project-features.md").read_text(encoding="utf-8"), "Dashboard-local mirror of the repo feature guide.", ), ( "frontend-dashboard-design.html", "本地前端工作台设计文档", - ROOT / "docs" / "frontend-dashboard-design.md", + (ROOT / "docs" / "frontend-dashboard-design.md").read_text(encoding="utf-8"), "Dashboard-local mirror of the UI and interaction specification.", ), ( "secure-code-index.html", "安全编码修复库索引", - ROOT / "05-defense" / "secure-code" / "INDEX.md", + (ROOT / "05-defense" / "secure-code" / "INDEX.md").read_text(encoding="utf-8"), "Dashboard-local mirror of the secure-code library index.", ), ] - for filename, title, source_path, description in docs: - write_text(docs_dir / filename, _dashboard_doc_page(title, source_path, description)) + + manifest_body = LOVART_VENDOR_MANIFEST.read_text(encoding="utf-8") if LOVART_VENDOR_MANIFEST.exists() else "{}" + sources.append( + ( + "design-source.html", + "Lovart 设计来源与本地化清单", + manifest_body, + "Local vendor manifest for the Lovart-derived dashboard shell.", + ) + ) + + for filename, title, body, description in sources: + write_text(docs_dir / filename, _dashboard_doc_page(title, body, description)) + + +def _write_design_source_manifest() -> None: + assets_dir = DASHBOARD_DIR / "assets" + ensure_dir(assets_dir) + manifest = json.loads(LOVART_VENDOR_MANIFEST.read_text(encoding="utf-8")) if LOVART_VENDOR_MANIFEST.exists() else {} + write_json(assets_dir / "design-source.json", manifest) + + +def _render_root_dashboard_shell() -> None: + assets_dir = DASHBOARD_DIR / "assets" + ensure_dir(assets_dir) + for filename in ("index.html",): + shutil.copy2(LOVART_TEMPLATE_DIR / filename, DASHBOARD_DIR / filename) + _copy_tree(LOVART_TEMPLATE_DIR / "assets", assets_dir) + + +def _render_legacy_dashboard_shell() -> None: + legacy_dir = DASHBOARD_DIR / "legacy" + _remove_path(legacy_dir) + ensure_dir(legacy_dir) + shutil.copy2(LEGACY_TEMPLATE_DIR / "index.html", legacy_dir / "index.html") + _copy_tree(LEGACY_TEMPLATE_DIR / "assets", legacy_dir / "assets") + for json_name in ROOT_JSON_FILES: + _sync_symlink(DASHBOARD_DIR / json_name, legacy_dir / json_name) + _sync_symlink(DASHBOARD_DIR / "runs", legacy_dir / "runs") + _sync_symlink(DASHBOARD_DIR / "docs", legacy_dir / "docs") + + +def _sync_run_bundles(runs: List[Dict[str, Any]]) -> None: + runs_dir = DASHBOARD_DIR / "runs" + ensure_dir(runs_dir) + for item in runs: + bundle_dir = Path(item.get("report_refs", {}).get("bundle_dir", "")) + if not bundle_dir.exists(): + continue + _sync_symlink(bundle_dir, runs_dir / item["run_id"]) def render_run(run: Dict[str, Any]) -> Dict[str, str]: @@ -445,32 +521,12 @@ def render_run(run: Dict[str, Any]) -> Dict[str, str]: def render_dashboard() -> Dict[str, str]: ensure_dir(DASHBOARD_DIR) - _write_dashboard_docs() advisory_records = load_json_dir(ADVISORIES_DIR) runs = load_json_dir(RUNS_DIR) advisory_map = {item["canonical_id"]: item for item in advisory_records if item.get("canonical_id")} profile_map = load_profiles() - runs_dir = DASHBOARD_DIR / "runs" - assets_dir = DASHBOARD_DIR / "assets" - ensure_dir(runs_dir) - ensure_dir(assets_dir) - for item in runs: - bundle_dir = Path(item.get("report_refs", {}).get("bundle_dir", "")) - if not bundle_dir.exists(): - continue - symlink_path = runs_dir / item["run_id"] - relative_target = os.path.relpath(bundle_dir, symlink_path.parent) - try: - if symlink_path.is_symlink() or symlink_path.exists(): - if symlink_path.is_symlink() and os.readlink(symlink_path) == relative_target: - pass - else: - symlink_path.unlink() - os.symlink(relative_target, symlink_path, target_is_directory=True) - else: - os.symlink(relative_target, symlink_path, target_is_directory=True) - except OSError: - continue + + _sync_run_bundles(runs) systems: Dict[str, Dict[str, Any]] = {} for advisory in advisory_records: @@ -567,7 +623,7 @@ def render_dashboard() -> Dict[str, str]: for item in runs: status = item.get("verification_status", "triage-manual") summary["statuses"][status] = summary["statuses"].get(status, 0) + 1 - summary["systems"] = sorted(systems.values(), key=lambda item: (-item["total"], item["system_id"])) + summary["systems"] = sorted(systems.values(), key=lambda entry: (-entry["total"], entry["system_id"])) summary["recent_failures"] = [ { "run_id": item["run_id"], @@ -579,1403 +635,21 @@ def render_dashboard() -> Dict[str, str]: for item in decorated_runs if item.get("verification_status") in {"triage-manual", "blocked-artifact", "blocked-destructive"} ][:20] + write_json(DASHBOARD_DIR / "summary.json", summary) write_json(DASHBOARD_DIR / "runs.json", decorated_runs) write_json(DASHBOARD_DIR / "systems.json", summary["systems"]) write_json(DASHBOARD_DIR / "advisories.json", {key: _advisory_meta(value) for key, value in advisory_map.items()}) write_json(DASHBOARD_DIR / "profiles.json", {key: _profile_meta(value) for key, value in profile_map.items()}) - styles_css = """ -:root { - --bg: #07111f; - --panel: rgba(9, 18, 32, 0.86); - --panel-2: rgba(10, 24, 44, 0.92); - --panel-soft: rgba(18, 32, 56, 0.74); - --border: rgba(137, 171, 214, 0.22); - --text: #f7fafc; - --muted: #9fb3ca; - --accent: #5eead4; - --accent-2: #ffb86b; - --accent-3: #90cdf4; - --danger: #ff7b7b; - --warning: #ffd166; - --success: #6ee7a5; - --shadow: 0 24px 80px rgba(1, 7, 20, 0.45); - --radius: 20px; -} + _write_dashboard_docs() + _write_design_source_manifest() + _render_root_dashboard_shell() + _render_legacy_dashboard_shell() -* { box-sizing: border-box; } -html, body { margin: 0; min-height: 100%; } -body { - font-family: "IBM Plex Sans", "Avenir Next", "Segoe UI", sans-serif; - background: - radial-gradient(circle at top left, rgba(94, 234, 212, 0.15), transparent 28%), - radial-gradient(circle at top right, rgba(255, 184, 107, 0.18), transparent 22%), - linear-gradient(145deg, #050c16 0%, #08111f 44%, #0d1c31 100%); - color: var(--text); - overflow-x: hidden; -} - -body::before { - content: ""; - position: fixed; - inset: 0; - pointer-events: none; - background-image: - linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px), - linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px); - background-size: 32px 32px; - mask-image: radial-gradient(circle at center, black 36%, transparent 78%); - opacity: 0.28; -} - -a { color: var(--accent); text-decoration: none; } -a:hover { text-decoration: underline; } -button, input, select { - font: inherit; -} - -.dashboard-shell { - position: relative; - max-width: 1640px; - margin: 0 auto; - padding: 32px 24px 40px; -} - -.hero { - position: sticky; - top: 0; - z-index: 20; - backdrop-filter: blur(18px); - background: linear-gradient(180deg, rgba(7, 17, 31, 0.94), rgba(7, 17, 31, 0.75)); - border: 1px solid var(--border); - border-radius: 28px; - padding: 24px 24px 20px; - box-shadow: var(--shadow); -} - -.hero-grid { - display: grid; - grid-template-columns: 1.6fr 1fr; - gap: 20px; - align-items: start; -} - -.eyebrow { - display: inline-flex; - align-items: center; - gap: 8px; - color: var(--muted); - font-size: 0.88rem; - letter-spacing: 0.12em; - text-transform: uppercase; -} - -.eyebrow::before { - content: ""; - width: 10px; - height: 10px; - border-radius: 999px; - background: radial-gradient(circle, var(--accent), rgba(94, 234, 212, 0.15)); - box-shadow: 0 0 24px rgba(94, 234, 212, 0.8); - animation: pulse 2.8s ease-in-out infinite; -} - -.hero h1 { - margin: 12px 0 10px; - font-family: "IBM Plex Serif", "Iowan Old Style", Georgia, serif; - font-size: clamp(2rem, 4vw, 3.5rem); - line-height: 1.02; -} - -.hero p { - margin: 0; - color: var(--muted); - max-width: 74ch; -} - -.hero-actions { - display: flex; - flex-wrap: wrap; - gap: 12px; - margin-top: 18px; -} - -.chip, .ghost-chip { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; - border-radius: 999px; - border: 1px solid var(--border); - padding: 10px 14px; - background: rgba(255,255,255,0.06); - color: var(--text); -} - -.ghost-chip { - background: rgba(255,255,255,0.04); -} - -.hero-meta { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 14px; -} - -.meta-card, .glass-panel { - background: var(--panel); - border: 1px solid var(--border); - border-radius: var(--radius); - box-shadow: var(--shadow); -} - -.meta-card { - padding: 18px; - min-height: 116px; -} - -.meta-card strong { - display: block; - color: var(--muted); - font-size: 0.84rem; - letter-spacing: 0.08em; - text-transform: uppercase; -} - -.meta-card span { - display: block; - margin-top: 10px; - font-size: 2rem; - font-weight: 700; -} - -.workspace { - display: grid; - grid-template-columns: 420px minmax(0, 1fr); - gap: 20px; - margin-top: 22px; -} - -.sidebar { - display: flex; - flex-direction: column; - gap: 18px; -} - -.panel-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - margin-bottom: 16px; -} - -.panel-header h2, .panel-header h3 { - margin: 0; - font-size: 1rem; - letter-spacing: 0.04em; - text-transform: uppercase; - color: var(--muted); -} - -.glass-panel { - padding: 18px; - background: - linear-gradient(180deg, rgba(255,255,255,0.04), transparent 35%), - var(--panel); -} - -.filters { - display: grid; - gap: 12px; -} - -.filters label { - display: grid; - gap: 6px; - color: var(--muted); - font-size: 0.9rem; -} - -.filters input, .filters select { - width: 100%; - background: rgba(255,255,255,0.05); - color: var(--text); - border: 1px solid rgba(159, 179, 202, 0.18); - border-radius: 14px; - padding: 12px 14px; -} - -.run-list { - display: grid; - gap: 12px; - max-height: calc(100vh - 460px); - overflow: auto; - padding-right: 4px; -} - -.run-card { - width: 100%; - text-align: left; - padding: 16px; - border-radius: 18px; - border: 1px solid rgba(159, 179, 202, 0.14); - background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0.03)); - color: var(--text); - cursor: pointer; - transition: transform 180ms ease, border-color 180ms ease, background 180ms ease; -} - -.run-card:hover, .run-card.is-active { - transform: translateY(-1px); - border-color: rgba(94, 234, 212, 0.42); - background: linear-gradient(180deg, rgba(94, 234, 212, 0.14), rgba(255,255,255,0.05)); -} - -.run-card-top, .flex-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; -} - -.run-card h4 { - margin: 10px 0 8px; - font-size: 1rem; - line-height: 1.35; -} - -.mini-muted { - color: var(--muted); - font-size: 0.86rem; -} - -.status-pill { - display: inline-flex; - align-items: center; - gap: 7px; - border-radius: 999px; - padding: 6px 10px; - font-size: 0.82rem; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.06em; - border: 1px solid transparent; -} - -.status-pill::before { - content: ""; - width: 8px; - height: 8px; - border-radius: 999px; - background: currentColor; - box-shadow: 0 0 16px currentColor; -} - -.status-blocked-artifact, .status-blocked-destructive { - color: var(--danger); - background: rgba(255, 123, 123, 0.14); - border-color: rgba(255, 123, 123, 0.24); -} - -.status-triage-manual, .status-suspected { - color: var(--warning); - background: rgba(255, 209, 102, 0.14); - border-color: rgba(255, 209, 102, 0.24); -} - -.status-verified-real { - color: var(--success); - background: rgba(110, 231, 165, 0.14); - border-color: rgba(110, 231, 165, 0.24); -} - -.status-verified-synthetic { - color: var(--accent-3); - background: rgba(144, 205, 244, 0.14); - border-color: rgba(144, 205, 244, 0.24); -} - -.status-default { - color: var(--accent); - background: rgba(94, 234, 212, 0.14); - border-color: rgba(94, 234, 212, 0.24); -} - -.detail-view { - display: grid; - gap: 18px; -} - -.detail-hero { - padding: 22px; - overflow: hidden; - position: relative; -} - -.detail-hero::after { - content: ""; - position: absolute; - inset: auto -20% -55% 25%; - height: 220px; - background: radial-gradient(circle, rgba(94, 234, 212, 0.2), transparent 55%); - pointer-events: none; -} - -.detail-headline { - margin: 8px 0 12px; - font-family: "IBM Plex Serif", "Iowan Old Style", Georgia, serif; - font-size: clamp(1.6rem, 3vw, 2.8rem); - line-height: 1.08; -} - -.tag-row, .link-row, .artifact-row { - display: flex; - flex-wrap: wrap; - gap: 10px; -} - -.tag { - display: inline-flex; - align-items: center; - padding: 7px 10px; - border-radius: 999px; - background: rgba(255,255,255,0.06); - border: 1px solid rgba(159, 179, 202, 0.18); - color: var(--text); - font-size: 0.86rem; -} - -.stat-grid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 12px; - margin-top: 18px; -} - -.stat-card { - padding: 14px; - border-radius: 16px; - background: rgba(255,255,255,0.04); - border: 1px solid rgba(159, 179, 202, 0.16); -} - -.stat-card strong { - display: block; - color: var(--muted); - font-size: 0.78rem; - text-transform: uppercase; - letter-spacing: 0.08em; -} - -.stat-card span { - display: block; - margin-top: 10px; - font-size: 1.15rem; - font-weight: 700; -} - -.detail-grid { - display: grid; - grid-template-columns: minmax(0, 1fr) 360px; - gap: 18px; -} - -.stack { - display: grid; - gap: 18px; -} - -.progress-strip { - display: grid; - gap: 12px; - margin-bottom: 16px; -} - -.progress-bar { - display: flex; - width: 100%; - min-height: 12px; - overflow: hidden; - border-radius: 999px; - background: rgba(255,255,255,0.08); - border: 1px solid rgba(159, 179, 202, 0.14); -} - -.progress-segment { - min-width: 10px; - transition: width 180ms ease; -} - -.progress-completed { background: linear-gradient(90deg, rgba(110, 231, 165, 0.9), rgba(94, 234, 212, 0.9)); } -.progress-blocked { background: linear-gradient(90deg, rgba(255, 123, 123, 0.95), rgba(255, 160, 122, 0.9)); } -.progress-failed { background: linear-gradient(90deg, rgba(255, 123, 123, 0.92), rgba(255, 209, 102, 0.88)); } -.progress-skipped { background: linear-gradient(90deg, rgba(255,255,255,0.22), rgba(159, 179, 202, 0.3)); } -.progress-planned { background: linear-gradient(90deg, rgba(144, 205, 244, 0.82), rgba(94, 234, 212, 0.72)); } -.progress-other { background: linear-gradient(90deg, rgba(255,255,255,0.18), rgba(255,255,255,0.1)); } - -.progress-legend { - display: flex; - flex-wrap: wrap; - gap: 10px; -} - -.progress-legend .tag { - gap: 7px; -} - -.progress-legend .swatch { - width: 10px; - height: 10px; - border-radius: 999px; - display: inline-block; -} - -.stage-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 12px; - margin-bottom: 18px; -} - -.stage-card { - padding: 14px; - border-radius: 16px; - background: rgba(255,255,255,0.04); - border: 1px solid rgba(159, 179, 202, 0.16); -} - -.stage-card strong { - display: block; - margin-bottom: 10px; -} - -.accordion { - overflow: hidden; -} - -.accordion > summary { - list-style: none; - cursor: pointer; - padding: 18px 20px; - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; -} - -.accordion > summary::-webkit-details-marker { display: none; } -.accordion > summary span { - font-size: 1rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--muted); -} - -.accordion .accordion-content { - padding: 0 20px 20px; - border-top: 1px solid rgba(159, 179, 202, 0.12); -} - -.timeline-list { - display: grid; - gap: 12px; -} - -.timeline-item { - display: grid; - grid-template-columns: 120px 180px minmax(0, 1fr); - gap: 12px; - padding: 12px 0; - border-bottom: 1px solid rgba(159, 179, 202, 0.12); -} - -.timeline-item:last-child { - border-bottom: 0; -} - -.timeline-step { - font-weight: 700; -} - -.artifact-group { - margin-bottom: 14px; -} - -.artifact-group h4 { - margin: 0 0 10px; - color: var(--muted); - font-size: 0.88rem; - text-transform: uppercase; - letter-spacing: 0.08em; -} - -.artifact-button { - display: inline-flex; - align-items: center; - gap: 8px; - margin: 0 10px 10px 0; - padding: 10px 12px; - border-radius: 14px; - border: 1px solid rgba(159, 179, 202, 0.16); - background: rgba(255,255,255,0.05); - color: var(--text); - cursor: pointer; -} - -.artifact-button:hover, .artifact-button.is-active { - border-color: rgba(94, 234, 212, 0.4); - background: rgba(94, 234, 212, 0.12); -} - -.log-viewer { - min-height: 420px; - display: grid; - gap: 14px; -} - -.viewer-toolbar { - display: flex; - flex-wrap: wrap; - justify-content: space-between; - gap: 10px; - align-items: center; -} - -.viewer-frame { - background: rgba(2, 8, 22, 0.88); - border: 1px solid rgba(159, 179, 202, 0.18); - border-radius: 16px; - min-height: 300px; - overflow: hidden; -} - -.viewer-frame pre { - margin: 0; - padding: 18px; - max-height: 560px; - overflow: auto; - font-family: "IBM Plex Mono", "SFMono-Regular", "Menlo", monospace; - font-size: 0.88rem; - line-height: 1.6; - color: #d6e5f5; - white-space: pre-wrap; -} - -.viewer-frame img { - display: block; - width: 100%; - height: auto; -} - -.gallery { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 14px; -} - -.gallery button { - all: unset; - cursor: pointer; - border-radius: 18px; - overflow: hidden; - border: 1px solid rgba(159, 179, 202, 0.18); - background: rgba(255,255,255,0.04); -} - -.gallery img { - display: block; - width: 100%; - aspect-ratio: 4 / 3; - object-fit: cover; -} - -.gallery figcaption { - padding: 10px 12px 14px; - color: var(--muted); - font-size: 0.84rem; -} - -.failure-callout { - padding: 16px 18px; - border-radius: 18px; - border: 1px solid rgba(255, 123, 123, 0.2); - background: rgba(255, 123, 123, 0.09); -} - -.json-block { - background: rgba(2, 8, 22, 0.72); - border-radius: 16px; - border: 1px solid rgba(159, 179, 202, 0.14); - padding: 16px; - overflow: auto; - font-family: "IBM Plex Mono", "SFMono-Regular", monospace; - font-size: 0.84rem; - line-height: 1.55; - color: #c9d8e8; -} - -.empty-state { - padding: 40px 24px; - text-align: center; - color: var(--muted); -} - -.failure-feed { - display: grid; - gap: 10px; -} - -.failure-item { - padding: 12px 14px; - border-radius: 16px; - background: rgba(255,255,255,0.04); - border: 1px solid rgba(159, 179, 202, 0.16); -} - -.system-grid { - display: grid; - gap: 10px; -} - -.system-card { - padding: 14px 16px; - border-radius: 16px; - background: rgba(255,255,255,0.04); - border: 1px solid rgba(159, 179, 202, 0.14); -} - -.meter { - position: relative; - height: 10px; - border-radius: 999px; - background: rgba(255,255,255,0.08); - overflow: hidden; - margin-top: 10px; -} - -.meter > span { - position: absolute; - inset: 0 auto 0 0; - width: var(--fill, 0%); - background: linear-gradient(90deg, var(--accent), var(--accent-2)); - border-radius: inherit; -} - -.sync-indicator { - display: inline-flex; - align-items: center; - gap: 8px; -} - -.sync-indicator strong { - color: var(--text); -} - -.dot { - width: 10px; - height: 10px; - border-radius: 999px; - background: var(--accent); - box-shadow: 0 0 18px rgba(94, 234, 212, 0.8); -} - -@keyframes pulse { - 0%, 100% { transform: scale(1); opacity: 0.88; } - 50% { transform: scale(1.35); opacity: 1; } -} - -@media (max-width: 1280px) { - .workspace, .detail-grid, .hero-grid { - grid-template-columns: 1fr; - } - - .stat-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - -@media (max-width: 760px) { - .dashboard-shell { - padding: 18px 14px 32px; - } - - .hero { - position: static; - } - - .stat-grid, .hero-meta { - grid-template-columns: 1fr; - } - - .timeline-item { - grid-template-columns: 1fr; - } -} -""" - write_text(assets_dir / "styles.css", styles_css) - - app_js = """ -const state = { - summary: null, - runs: [], - systems: [], - advisories: {}, - profiles: {}, - selectedRunId: null, - selectedArtifact: null, - filters: { search: "", system: "", status: "", family: "" }, - autoRefresh: true, - refreshMs: 5000, - refreshHandle: null, -}; - -const $ = (id) => document.getElementById(id); -const statusClass = (status) => `status-pill ${({ - "blocked-artifact": "status-blocked-artifact", - "blocked-destructive": "status-blocked-destructive", - "triage-manual": "status-triage-manual", - "verified-real": "status-verified-real", - "verified-synthetic": "status-verified-synthetic", - "suspected": "status-suspected", - "completed": "status-verified-real", - "failed": "status-blocked-artifact", - "skipped": "status-triage-manual" -})[status] || "status-default"}`; - -function escapeHtml(value) { - return String(value ?? "") - .replaceAll("&", "&") - .replaceAll("<", "<") - .replaceAll(">", ">") - .replaceAll('"', """); -} - -function timeAgo(value) { - if (!value) return "-"; - const diff = Date.now() - new Date(value).getTime(); - if (Number.isNaN(diff)) return value; - const seconds = Math.floor(diff / 1000); - if (seconds < 60) return `${seconds}s ago`; - const minutes = Math.floor(seconds / 60); - if (minutes < 60) return `${minutes}m ago`; - const hours = Math.floor(minutes / 60); - if (hours < 24) return `${hours}h ago`; - const days = Math.floor(hours / 24); - return `${days}d ago`; -} - -async function fetchJson(url) { - const response = await fetch(`${url}?t=${Date.now()}`, { cache: "no-store" }); - if (!response.ok) { - throw new Error(`${url} -> ${response.status}`); - } - return response.json(); -} - -async function loadData(preserveSelection = true) { - $("syncState").innerHTML = `Refreshing${new Date().toLocaleTimeString()}`; - const previousRun = state.selectedRunId; - try { - const [summary, runs, systems, advisories, profiles] = await Promise.all([ - fetchJson("./summary.json"), - fetchJson("./runs.json"), - fetchJson("./systems.json"), - fetchJson("./advisories.json"), - fetchJson("./profiles.json"), - ]); - state.summary = summary; - state.runs = runs; - state.systems = systems; - state.advisories = advisories; - state.profiles = profiles; - hydrateFilterOptions(); - - const hashRun = location.hash.startsWith("#run=") ? location.hash.replace("#run=", "") : null; - const selectedCandidate = preserveSelection ? (hashRun || previousRun) : hashRun; - if (selectedCandidate && runs.some((item) => item.run_id === selectedCandidate)) { - state.selectedRunId = selectedCandidate; - } else { - state.selectedRunId = runs[0]?.run_id || null; - } - - renderDashboard(); - $("syncState").innerHTML = `Live${summary.generated_at || new Date().toISOString()}`; - } catch (error) { - $("syncState").innerHTML = `Load Failed${escapeHtml(error.message)}`; - $("runList").innerHTML = `
Dashboard load failed: ${escapeHtml(error.message)}
`; - $("detailRoot").innerHTML = `
Unable to load dashboard data. Check generated JSON and local static server state.
`; - } -} - -function filteredRuns() { - return state.runs.filter((item) => { - if (state.filters.system && item.system_id !== state.filters.system) return false; - if (state.filters.status && item.verification_status !== state.filters.status) return false; - if (state.filters.family && item.repro_profile_id !== state.filters.family) return false; - if (!state.filters.search) return true; - const advisoryTitle = item.advisory_meta?.title || ""; - const haystack = [item.run_id, item.advisory_id, item.system_id, item.repro_profile_id, advisoryTitle] - .join(" ") - .toLowerCase(); - return haystack.includes(state.filters.search); - }); -} - -function renderMetrics() { - const metrics = [ - { label: "Advisories", value: state.summary?.advisory_count ?? 0 }, - { label: "Run Bundles", value: state.summary?.run_count ?? 0 }, - ...Object.entries(state.summary?.statuses || {}).map(([label, value]) => ({ label, value })), - ]; - $("metrics").innerHTML = metrics - .map((item) => `
${escapeHtml(item.label)}${escapeHtml(item.value)}
`) - .join(""); -} - -function renderSystemCoverage() { - $("systemCoverage").innerHTML = state.systems - .map((system) => { - const total = Math.max(system.total || 0, 1); - const verified = (system.verified_real || 0) + (system.verified_synthetic || 0); - const fill = Math.round((verified / total) * 100); - return ` -
-
- ${escapeHtml(system.display_name || system.system_id)} - ${escapeHtml(system.browser_present || 0)}/${escapeHtml(system.browser_required || 0)} browser -
-
${escapeHtml(system.system_id)} · latest ${escapeHtml(system.latest_update || "-")}
-
- real ${escapeHtml(system.verified_real || 0)} - synthetic ${escapeHtml(system.verified_synthetic || 0)} - blocked ${escapeHtml(system.blocked || 0)} - manual ${escapeHtml(system.manual || 0)} -
-
-
- `; - }) - .join(""); -} - -function renderFailures() { - const failures = state.summary?.recent_failures || []; - $("failureFeed").innerHTML = failures.length - ? failures - .map((item) => ` -
-
- ${escapeHtml(item.run_id)} - ${escapeHtml(item.status)} -
-
${escapeHtml(item.title || item.advisory_id)}
-
${escapeHtml(item.blocked_reason || "-")}
-
- `) - .join("") - : `
No recent blockers.
`; -} - -function renderRunList() { - const filtered = filteredRuns(); - $("runCount").textContent = `${filtered.length} shown`; - $("runList").innerHTML = filtered.length - ? filtered - .map((item) => { - const active = item.run_id === state.selectedRunId ? "is-active" : ""; - const title = item.advisory_meta?.title || item.advisory_id; - const reasoning = item.reasoning_lines?.[0] || item.blocked_reason || ""; - const browserLabel = item.browser_evidence?.present ? "ready" : (item.browser_evidence?.required ? "required" : "n/a"); - return ` - - `; - }) - .join("") - : `
No runs match the current filters.
`; - - document.querySelectorAll("[data-run-id]").forEach((button) => { - button.addEventListener("click", () => { - state.selectedRunId = button.dataset.runId; - location.hash = `run=${state.selectedRunId}`; - renderRunList(); - renderDetail(); - }); - }); -} - -function renderDashboard() { - renderMetrics(); - renderSystemCoverage(); - renderFailures(); - renderRunList(); - renderDetail(); -} - -function setFilterListeners() { - [["searchInput", "search"], ["systemFilter", "system"], ["statusFilter", "status"], ["familyFilter", "family"]].forEach(([id, key]) => { - $(id).addEventListener("input", (event) => { - state.filters[key] = String(event.target.value || "").trim().toLowerCase(); - if (key !== "search") { - state.filters[key] = String(event.target.value || ""); - } - renderRunList(); - }); - }); -} - -function hydrateFilterOptions() { - const distinct = (items) => [...new Set(items.filter(Boolean))].sort(); - const patchOptions = (id, values) => { - const control = $(id); - const current = control.value; - control.innerHTML = control.dataset.base; - control.innerHTML += distinct(values).map((value) => ``).join(""); - control.value = current; - }; - patchOptions("systemFilter", state.runs.map((item) => item.system_id)); - patchOptions("statusFilter", state.runs.map((item) => item.verification_status)); - patchOptions("familyFilter", state.runs.map((item) => item.repro_profile_id)); -} - -function defaultArtifact(run) { - const preference = ["attack", "requests", "container", "browser", "baseline", "compose", "reports"]; - for (const key of preference) { - const group = (run.artifact_groups || []).find((item) => item.key === key && item.items?.length); - if (!group) continue; - const preferredText = group.items.find((item) => item.kind === "text"); - return preferredText || group.items[0]; - } - return null; -} - -function totalProgress(progress) { - const values = Object.values(progress || {}).map((value) => Number(value || 0)); - return values.reduce((sum, value) => sum + value, 0); -} - -function renderProgressStrip(progress) { - const total = totalProgress(progress); - if (!total) { - return ` -
-
-
No timeline progress recorded.
-
- `; - } - const order = [ - ["completed", "Completed", "progress-completed"], - ["blocked", "Blocked", "progress-blocked"], - ["failed", "Failed", "progress-failed"], - ["skipped", "Skipped", "progress-skipped"], - ["planned", "Planned", "progress-planned"], - ["other", "Other", "progress-other"], - ]; - const segments = order - .filter(([key]) => Number(progress?.[key] || 0) > 0) - .map(([key, _label, klass]) => { - const count = Number(progress?.[key] || 0); - const pct = Math.max((count / total) * 100, 4); - return `
`; - }) - .join(""); - const legend = order - .filter(([key]) => Number(progress?.[key] || 0) > 0) - .map(([key, label, klass]) => ` - - - ${escapeHtml(label)} ${escapeHtml(progress?.[key] || 0)} - - `) - .join(""); - return ` -
-
${segments}
-
${legend}
-
- `; -} - -function renderStageCards(run) { - const timeline = run.timeline || []; - if (!timeline.length) { - return `
No stage records available.
`; - } - return ` -
- ${timeline.map((item) => ` -
- ${escapeHtml(item.step || "-")} -
${escapeHtml(item.status || "unknown")}
-
${escapeHtml(item.detail || "-")}
-
${escapeHtml(item.at || "-")}
-
- `).join("")} -
- `; -} - -async function openArtifact(href, label, kind) { - state.selectedArtifact = { href, label, kind }; - document.querySelectorAll(".artifact-button").forEach((button) => { - button.classList.toggle("is-active", button.dataset.href === href); - }); - $("artifactLabel").textContent = label; - $("artifactOpen").href = href; - $("artifactMeta").textContent = href; - try { - if (kind === "image") { - $("artifactViewer").innerHTML = `${escapeHtml(label)}`; - return; - } - if (href.endsWith(".html")) { - $("artifactViewer").innerHTML = ``; - return; - } - const response = await fetch(`${href}?t=${Date.now()}`, { cache: "no-store" }); - if (!response.ok) throw new Error(`${href} -> ${response.status}`); - const text = await response.text(); - let formatted = text; - if (href.endsWith(".json")) { - try { - formatted = JSON.stringify(JSON.parse(text), null, 2); - } catch (_error) { - } - } - $("artifactViewer").innerHTML = `
${escapeHtml(formatted)}
`; - } catch (error) { - $("artifactViewer").innerHTML = `
Artifact load failed: ${escapeHtml(error.message)}
`; - } -} - -function renderDetail() { - const run = state.runs.find((item) => item.run_id === state.selectedRunId); - if (!run) { - $("detailRoot").innerHTML = `
Select a run to inspect full timeline, logs, sources, and reasoning.
`; - return; - } - - const advisory = run.advisory_meta || {}; - const profile = run.profile_meta || {}; - const screenshotItems = (run.artifact_groups || []) - .find((group) => group.key === "browser") - ?.items.filter((item) => item.kind === "image") || []; - - $("detailRoot").innerHTML = ` -
-
Local Verification Workspace
-
- ${escapeHtml(run.verification_status)} -
- ${escapeHtml(run.system_id)} - ${escapeHtml(run.repro_profile_id)} - ${escapeHtml(run.artifact_mode)} - ${escapeHtml(run.verification_mode)} - ${escapeHtml(run.target_env || "local-docker")} -
-
-

${escapeHtml(advisory.title || run.advisory_id)}

-

${escapeHtml(advisory.summary || "No summary available.")}

- -
-
Timeline Steps${escapeHtml(run.timeline?.length || 0)}
-
Artifacts${escapeHtml((run.artifact_groups || []).reduce((sum, group) => sum + group.count, 0))}
-
Browser${run.browser_evidence?.present ? "Ready" : (run.browser_evidence?.required ? "Required" : "Optional")}
-
Finished${escapeHtml(timeAgo(run.finished_at))}
-
-
- -
-
-
- Progress Timeline${escapeHtml(run.timeline?.length || 0)} steps -
- ${renderProgressStrip(run.progress)} - ${renderStageCards(run)} -
- ${(run.timeline || []).map((item) => ` -
-
${escapeHtml(item.at || "-")}
-
${escapeHtml(item.step || "-")}
-
-
${escapeHtml(item.status || "unknown")}
-
${escapeHtml(item.detail || "-")}
-
-
- `).join("") || `
No timeline items available.
`} -
-
-
- -
- Attack Plan & Reasoning${escapeHtml(profile.vuln_family || "unknown")} -
- ${run.blocked_reason ? `
Failure reason
${escapeHtml(run.blocked_reason)}
` : ""} -
- destructive risk ${escapeHtml(profile.destructive_risk || "-")} - cleanup ${escapeHtml(profile.cleanup_policy || "-")} - targets ${(profile.allowed_target_types || []).join(", ") || "-"} -
-
- ${(run.reasoning_lines || []).map((line) => `
${escapeHtml(line)}
`).join("")} -
-
- ${(profile.success_criteria || []).map((line) => `${escapeHtml(line)}`).join("")} -
-
-
- -
- Evidence Explorer${escapeHtml((run.artifact_groups || []).length)} groups -
- ${(run.artifact_groups || []).map((group) => ` -
-

${escapeHtml(group.label)} · ${escapeHtml(group.count)}

-
- ${group.items.map((item) => ` - - `).join("")} -
-
- `).join("") || `
No artifacts linked for this run.
`} - ${screenshotItems.length ? ` - - ` : ""} -
-
- -
- Live Log Viewer${state.selectedArtifact ? "active" : "idle"} -
-
-
-
- ${escapeHtml(state.selectedArtifact?.label || "Select an artifact")} -
${escapeHtml(state.selectedArtifact?.href || "Artifacts and logs can be previewed here.")}
-
-
- Open artifact - -
-
-
Select a report, log, JSON, screenshot, or timeline file to preview it here.
-
-
-
-
- -
-
- Sources & Fix Topics${escapeHtml((advisory.secondary_source_urls || []).length + (advisory.official_source_url ? 1 : 0))} links -
-
- ${(advisory.aliases || []).map((alias) => `${escapeHtml(alias)}`).join("")} -
-
- ${advisory.official_source_url ? `${escapeHtml(advisory.official_source_url)}` : `
No official source linked.
`} - ${(advisory.secondary_source_urls || []).map((ref) => `${escapeHtml(ref)}`).join("")} -
-
- ${(advisory.secure_code_topics || []).map((topic) => `${escapeHtml(topic)}`).join("")} -
-
-
- -
- Run JSONraw -
${escapeHtml(JSON.stringify(run, null, 2))}
-
- -
- Advisory JSONraw -
${escapeHtml(JSON.stringify(advisory, null, 2))}
-
- -
- Profile JSONraw -
${escapeHtml(JSON.stringify(profile, null, 2))}
-
-
-
- `; - - document.querySelectorAll(".artifact-button").forEach((button) => { - button.addEventListener("click", () => openArtifact(button.dataset.href, button.dataset.label, button.dataset.kind)); - }); - - $("refreshArtifact")?.addEventListener("click", () => { - if (state.selectedArtifact) { - openArtifact(state.selectedArtifact.href, state.selectedArtifact.label, state.selectedArtifact.kind); - } - }); - - if (!state.selectedArtifact || !(run.artifact_groups || []).some((group) => group.items.some((item) => item.href === state.selectedArtifact.href))) { - const artifact = defaultArtifact(run); - if (artifact) { - openArtifact(artifact.href, artifact.label, artifact.kind); - } - } else { - openArtifact(state.selectedArtifact.href, state.selectedArtifact.label, state.selectedArtifact.kind); - } -} - -function attachGlobalActions() { - $("searchInput").addEventListener("input", (event) => { - state.filters.search = String(event.target.value || "").trim().toLowerCase(); - renderRunList(); - }); - [["systemFilter", "system"], ["statusFilter", "status"], ["familyFilter", "family"]].forEach(([id, key]) => { - $(id).addEventListener("input", (event) => { - state.filters[key] = String(event.target.value || ""); - renderRunList(); - }); - }); - $("refreshDashboard").addEventListener("click", () => loadData(false)); - $("autoRefresh").addEventListener("change", (event) => { - state.autoRefresh = Boolean(event.target.checked); - startRefreshLoop(); - }); -} - -function startRefreshLoop() { - if (state.refreshHandle) { - clearInterval(state.refreshHandle); - state.refreshHandle = null; - } - if (!state.autoRefresh) return; - state.refreshHandle = setInterval(() => loadData(true), state.refreshMs); -} - -async function init() { - ["systemFilter", "statusFilter", "familyFilter"].forEach((id) => { - $(id).dataset.base = $(id).innerHTML; - }); - attachGlobalActions(); - await loadData(false); - startRefreshLoop(); - window.addEventListener("hashchange", () => loadData(false)); -} - -document.addEventListener("DOMContentLoaded", init); -""" - write_text(assets_dir / "app.js", app_js) - - html_page = """ - - - - - - websafe authorized lab dashboard - - - -
-
-
-
-
Authorized Lab Dashboard
-

本地攻防实证工作台

-

面向授权实验场景的本地静态前端。聚合 advisory、run bundle、日志、浏览器证据、失败原因、利用思路与源头信息,并支持可折叠细节与自动刷新。

-
- - - Open Summary JSON - Open Feature Docs -
-
-
-
-

Sync State

-
BootingLoading generated JSON
-
-
-
-
-
- -
- - -
-
Select a run to inspect full details.
-
-
-
- - - -""" - write_text(DASHBOARD_DIR / "index.html", html_page) return { "dashboard_dir": str(DASHBOARD_DIR), - "index_html": str(DASHBOARD_DIR / "index.html"), + "index": str(DASHBOARD_DIR / "index.html"), + "legacy_index": str(DASHBOARD_DIR / "legacy" / "index.html"), + "summary_json": str(DASHBOARD_DIR / "summary.json"), }