diff --git a/guarddog_nexus/web/static/app.js b/guarddog_nexus/web/static/app.js index 8cee215..00dd8db 100644 --- a/guarddog_nexus/web/static/app.js +++ b/guarddog_nexus/web/static/app.js @@ -1,16 +1,5 @@ // GuardDog Nexus — shared UI utilities -function toggleFindings() { - var container = document.getElementById('findings-container'); - if (!container) return; - var details = container.querySelectorAll('details'); - if (details.length === 0) return; - var isOpen = details[0].open; - details.forEach(function (d) { d.open = !isOpen; }); - var btn = document.querySelector('.toggle-all-btn'); - if (btn) btn.textContent = isOpen ? 'Expand All' : 'Collapse All'; -} - function copyCode(btn, codeId) { var el = document.getElementById(codeId); if (!el) return; diff --git a/guarddog_nexus/web/static/style.css b/guarddog_nexus/web/static/style.css index 1180e3a..ee296a4 100644 --- a/guarddog_nexus/web/static/style.css +++ b/guarddog_nexus/web/static/style.css @@ -1,130 +1,71 @@ /* GuardDog Nexus — Web UI styles */ -/* Status badges */ -.flagged { - color: var(--pico-color-red-400); - font-weight: bold; -} +/* ------------------------------------------------------------------ */ +/* Status / severity colours */ +/* ------------------------------------------------------------------ */ +.flagged { color: var(--pico-color-red-400); font-weight: bold; } +.clean { color: var(--pico-color-green-400); } -.clean { - color: var(--pico-color-green-400); -} +.status-pending { color: var(--pico-color-yellow-400); } +.status-scanning { color: var(--pico-color-blue-400); animation: pulse 1.5s ease-in-out infinite; } +.status-completed { color: var(--pico-color-green-400); } +.status-failed { color: var(--pico-color-red-400); } -.status-pending { - color: var(--pico-color-yellow-400); -} +.severity-WARNING { color: var(--pico-color-yellow-400); } +.severity-ERROR { color: var(--pico-color-red-400); } -.status-scanning { - color: var(--pico-color-blue-400); - animation: pulse 1.5s ease-in-out infinite; -} - -.status-completed { - color: var(--pico-color-green-400); -} - -.status-failed { - color: var(--pico-color-red-400); -} - -/* Severity colors */ -.severity-WARNING { - color: var(--pico-color-yellow-400); -} - -.severity-ERROR { - color: var(--pico-color-red-400); -} - -/* Finding cards */ -.finding-card { - margin-bottom: 0.5rem; - padding: 0.5rem; - border-left: 3px solid; - transition: opacity 0.2s; -} - -.finding-card:hover { +/* ------------------------------------------------------------------ */ +/* Dashboard mini-bar */ +/* ------------------------------------------------------------------ */ +.stat-minibar { + display: flex; + gap: 1.5rem; + padding: 0.6rem 0; + margin-bottom: 1.5rem; + border-bottom: 1px solid var(--pico-color-gray-500); + font-size: 0.9rem; opacity: 0.9; } -.finding-card.WARNING { - border-left-color: var(--pico-color-yellow-400); -} - -.finding-card.ERROR { - border-left-color: var(--pico-color-red-400); -} - -.finding-card.INFO { - border-left-color: var(--pico-color-blue-400); -} - -/* Tables */ -table { - font-size: 0.9rem; -} - -/* Nav */ -nav { +/* Dashboard block grid (2 cols → 1 on mobile) */ +.dashboard-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; margin-bottom: 1rem; } -/* Stats grid */ -.stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: 1rem; - margin-bottom: 2rem; -} - -.stat-card { - text-align: center; +.dash-block { padding: 1rem; - transition: transform 0.2s, box-shadow 0.2s; + margin-bottom: 0; } -.stat-card:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +.dash-block h3 { + margin-top: 0; + font-size: 0.95rem; } -/* Severity ratio bar */ -.severity-bar { - display: flex; - height: 8px; - border-radius: 4px; - overflow: hidden; - margin-top: 4px; - margin-bottom: 2rem; +.dash-block-warn { + border-left: 3px solid var(--pico-color-red-400); } -.severity-bar .bar-error { - background: var(--pico-color-red-400); - transition: width 0.5s ease; -} +/* ------------------------------------------------------------------ */ +/* Tables */ +/* ------------------------------------------------------------------ */ +table { font-size: 0.9rem; } +table.compact { font-size: 0.82rem; } +table.compact th, +table.compact td { padding: 0.35rem 0.5rem; } -.severity-bar .bar-warning { - background: var(--pico-color-yellow-400); - transition: width 0.5s ease; -} - -.severity-bar-labels { - display: flex; - justify-content: space-between; - font-size: 0.75rem; - margin-top: 2px; - margin-bottom: 2rem; -} - -/* Scan activity heatmap */ +/* ------------------------------------------------------------------ */ +/* Heatmap */ +/* ------------------------------------------------------------------ */ .heatmap { display: flex; align-items: flex-end; gap: 2px; height: 40px; - margin-top: 4px; - margin-bottom: 2rem; + margin: 0.4rem 0 0 0; } .heatmap-day { @@ -139,12 +80,9 @@ nav { border-radius: 2px 2px 0 0; opacity: 0.8; transition: height 0.3s ease, opacity 0.2s; - cursor: default; } -.heatmap-day:hover .bar { - opacity: 1; -} +.heatmap-day:hover .bar { opacity: 1; } .heatmap-day .tooltip { display: none; @@ -162,138 +100,152 @@ nav { margin-bottom: 4px; } -.heatmap-day:hover .tooltip { - display: block; +.heatmap-day:hover .tooltip { display: block; } + +/* ------------------------------------------------------------------ */ +/* Scan info block (detail page) */ +/* ------------------------------------------------------------------ */ +.scan-info-block { + padding: 1rem 1.25rem; + margin-bottom: 1.5rem; } -/* Top rules bar chart */ -.top-rules-chart { - margin-bottom: 2rem; +.scan-info-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 0.6rem 1.5rem; } -.top-rules-chart .rule-bar-row { +.sha256 { + font-size: 0.72rem; + word-break: break-all; +} + +.scan-error { + margin-top: 0.75rem; + padding: 0.5rem 0.75rem; + background: rgba(var(--pico-color-red-400), 0.1); + border-left: 3px solid var(--pico-color-red-400); + border-radius: 4px; + font-size: 0.85rem; +} + +/* ------------------------------------------------------------------ */ +/* Finding blocks (replaces accordion details/summary) */ +/* ------------------------------------------------------------------ */ +.finding-block { + margin-bottom: 0.75rem; + border: 1px solid var(--pico-color-gray-600); + border-radius: 6px; + overflow: hidden; +} + +.finding-summary { display: flex; align-items: center; - margin-bottom: 0.4rem; -} - -.top-rules-chart .rule-name { - flex: 0 0 200px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - text-align: right; - padding-right: 0.75rem; - font-size: 0.85rem; -} - -.top-rules-chart .rule-bar-container { - flex: 1; - background: var(--pico-color-gray-500); - border-radius: 4px; - overflow: hidden; - height: 20px; -} - -.top-rules-chart .rule-bar { - height: 100%; - background: var(--pico-color-blue-400); - border-radius: 4px; - transition: width 0.5s ease; - min-width: 2px; -} - -.top-rules-chart .rule-count { - flex: 0 0 50px; - padding-left: 0.5rem; - font-size: 0.85rem; -} - -/* Sticky nav */ -nav.sticky { - position: sticky; - top: 0; - z-index: 100; - background: var(--pico-color-dark); - padding: 0.5rem 0; - border-bottom: 1px solid var(--pico-color-gray-500); -} - -/* Breadcrumbs */ -.breadcrumbs { - margin-bottom: 1rem; - font-size: 0.85rem; - opacity: 0.7; -} - -.breadcrumbs a { - text-decoration: none; -} - -.breadcrumbs .separator { - margin: 0 0.5rem; - opacity: 0.5; -} - -/* Empty states */ -.empty-state { - text-align: center; - padding: 2rem 1rem; - opacity: 0.5; - font-style: italic; -} - -/* Filter bar */ -.filter-bar { - display: flex; - flex-wrap: wrap; gap: 0.5rem; - margin-bottom: 1rem; - align-items: center; + padding: 0.5rem 0.75rem; + background: var(--pico-color-gray-650); + font-size: 0.9rem; + border-bottom: 1px solid var(--pico-color-gray-600); } -.filter-bar input[type="text"], -.filter-bar select { - margin-bottom: 0; +.finding-body { + padding: 0.75rem; } -.filter-bar .filter-btn { - margin-bottom: 0; +.finding-body p { + margin-bottom: 0.5rem; } -/* Sortable columns */ -th.sortable { - cursor: pointer; - user-select: none; -} - -th.sortable:hover { - background: var(--pico-color-gray-600); -} - -th.sortable .sort-icon { - margin-left: 0.25rem; - opacity: 0.3; -} - -th.sortable.active .sort-icon { - opacity: 1; -} - -/* Collapsible findings */ -.finding-details { - margin-top: 0.5rem; -} - -.finding-details pre { +.finding-body pre { background: var(--pico-color-gray-700); padding: 0.5rem; border-radius: 4px; overflow-x: auto; font-size: 0.8rem; + margin-bottom: 0; +} + +.finding-header-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; +} + +.finding-header-row h2 { + margin-bottom: 0; +} + +/* ------------------------------------------------------------------ */ +/* LLM report — verdict-based colour scheme */ +/* ------------------------------------------------------------------ */ +.llm-report { + margin-top: 0.75rem; + padding: 0.8rem 1rem; + border-radius: 6px; + font-size: 0.85rem; + line-height: 1.55; + border-left: 4px solid var(--pico-color-gray-500); + background: var(--pico-color-gray-750); +} + +.llm-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.llm-badge { + display: inline-block; + padding: 0.1rem 0.5rem; + border-radius: 3px; + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.llm-badge-safe { background: #1a5c2a; color: #4ade80; } +.llm-badge-suspicious { background: #5c4a1a; color: #facc15; } +.llm-badge-malicious { background: #5c1a1a; color: #f87171; } + +.llm-safe { border-left-color: var(--pico-color-green-400); background: rgba(74,222,128,0.06); } +.llm-suspicious { border-left-color: var(--pico-color-yellow-400); background: rgba(250,204,21,0.06); } +.llm-malicious { border-left-color: var(--pico-color-red-400); background: rgba(248,113,113,0.08); } + +.llm-severity { + font-size: 0.72rem; + opacity: 0.6; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.llm-summary { + font-style: italic; + margin-bottom: 0.4rem; + color: var(--pico-color-zinc-300); +} + +.llm-analysis { + margin-bottom: 0; + white-space: pre-line; +} + +.llm-actions { margin-top: 0.5rem; } +.llm-actions button { font-size: 0.8rem; } + +/* ------------------------------------------------------------------ */ +/* Shared controls */ +/* ------------------------------------------------------------------ */ +.code-toolbar { + display: flex; + justify-content: flex-end; + margin-bottom: 0.25rem; } -/* Copy button */ .copy-btn { cursor: pointer; background: none; @@ -305,16 +257,57 @@ th.sortable.active .sort-icon { transition: background 0.2s; } -.copy-btn:hover { - background: var(--pico-color-gray-600); +.copy-btn:hover { background: var(--pico-color-gray-600); } +.copy-btn.copied { color: var(--pico-color-green-400); border-color: var(--pico-color-green-400); } + +.toggle-all-btn { + font-size: 0.8rem; + cursor: pointer; + background: none; + border: 1px solid var(--pico-color-gray-500); + padding: 0.2rem 0.6rem; + border-radius: 3px; + color: var(--pico-color-gray-300); } -.copy-btn.copied { - color: var(--pico-color-green-400); - border-color: var(--pico-color-green-400); +.toggle-all-btn:hover { background: var(--pico-color-gray-600); } + +.htmx-indicator { display: inline; } + +/* ------------------------------------------------------------------ */ +/* Nav / breadcrumbs / empty state */ +/* ------------------------------------------------------------------ */ +nav { margin-bottom: 1rem; } +nav.sticky { position: sticky; top: 0; z-index: 100; background: var(--pico-color-dark); padding: 0.5rem 0; border-bottom: 1px solid var(--pico-color-gray-500); } + +.breadcrumbs { margin-bottom: 1rem; font-size: 0.85rem; opacity: 0.7; } +.breadcrumbs a { text-decoration: none; } +.breadcrumbs .separator { margin: 0 0.5rem; opacity: 0.5; } + +.empty-state { text-align: center; padding: 2rem 1rem; opacity: 0.5; font-style: italic; } + +/* ------------------------------------------------------------------ */ +/* Filter bar / sortable cols */ +/* ------------------------------------------------------------------ */ +.filter-bar { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 1rem; + align-items: center; } -/* Spinner for scanning status */ +.filter-bar input[type="text"], +.filter-bar select { margin-bottom: 0; } + +th.sortable { cursor: pointer; user-select: none; } +th.sortable:hover { background: var(--pico-color-gray-600); } +th.sortable .sort-icon { margin-left: 0.25rem; opacity: 0.3; } +th.sortable.active .sort-icon { opacity: 1; } + +/* ------------------------------------------------------------------ */ +/* Spinner / animations */ +/* ------------------------------------------------------------------ */ .spinner { display: inline-block; width: 12px; @@ -327,156 +320,33 @@ th.sortable.active .sort-icon { vertical-align: middle; } -@keyframes spin { - to { - transform: rotate(360deg); - } -} +@keyframes spin { to { transform: rotate(360deg); } } +@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } -@keyframes pulse { - 0%, 100% { - opacity: 1; - } - 50% { - opacity: 0.5; - } -} - -/* Finding header row */ -.finding-header-row { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 0.5rem; -} - -/* Finding summary */ -.finding-summary { - cursor: pointer; - list-style: none; - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.25rem 0; -} - -/* Finding summary hint */ -.finding-summary-hint { - margin-left: auto; - font-size: 0.8rem; - opacity: 0.5; -} - -/* Code block toolbar */ -.code-toolbar { - display: flex; - justify-content: flex-end; - margin-bottom: 0.25rem; -} - -/* LLM report */ -.llm-report { - margin-top: 0.75rem; - padding: 0.6rem 0.8rem; - background: var(--pico-color-gray-700); - border-radius: 6px; - font-size: 0.85rem; - line-height: 1.5; - border-left: 3px solid var(--pico-color-blue-400); -} - -.llm-report strong { - color: var(--pico-color-blue-300); -} - -.verdict-safe { - color: var(--pico-color-green-400); - font-weight: bold; -} - -.verdict-suspicious { - color: var(--pico-color-yellow-400); - font-weight: bold; -} - -.verdict-malicious { - color: var(--pico-color-red-400); - font-weight: bold; -} - -.llm-actions { - margin-top: 0.5rem; -} - -.llm-actions button { - font-size: 0.8rem; -} - -/* htmx indicator */ -.htmx-indicator { - display: inline; -} -.toggle-all-btn { - font-size: 0.8rem; - margin-bottom: 0.5rem; - cursor: pointer; - background: none; - border: 1px solid var(--pico-color-gray-500); - padding: 0.2rem 0.6rem; - border-radius: 3px; - color: var(--pico-color-gray-300); -} - -.toggle-all-btn:hover { - background: var(--pico-color-gray-600); -} - -/* Responsive */ +/* ------------------------------------------------------------------ */ +/* Responsive */ +/* ------------------------------------------------------------------ */ @media (max-width: 768px) { - .stats-grid { - grid-template-columns: repeat(2, 1fr); - } - - .filter-bar { - flex-direction: column; - align-items: stretch; - } - - .top-rules-chart .rule-name { - flex: 0 0 100px; - } - - nav ul { - flex-wrap: wrap; - } - - table { - font-size: 0.8rem; - } - - th, td { - padding: 0.35rem 0.5rem; - } + .dashboard-grid { grid-template-columns: 1fr; } + .scan-info-grid { grid-template-columns: 1fr 1fr; } + .stat-minibar { flex-wrap: wrap; gap: 0.75rem; } + .filter-bar { flex-direction: column; align-items: stretch; } + nav ul { flex-wrap: wrap; } + table, table.compact { font-size: 0.78rem; } + th, td { padding: 0.3rem 0.4rem; } } @media (max-width: 480px) { - .stats-grid { - grid-template-columns: 1fr; - } - - .stat-card { - padding: 0.75rem; - } + .scan-info-grid { grid-template-columns: 1fr; } + .stat-minibar { font-size: 0.8rem; } } -/* Print styles */ +/* ------------------------------------------------------------------ */ +/* Print */ +/* ------------------------------------------------------------------ */ @media print { - nav, .filter-bar, .copy-btn, .toggle-all-btn, nav.sticky { - display: none !important; - } - - body { - background: white; - color: black; - } + nav, .filter-bar, .copy-btn, .toggle-all-btn, nav.sticky, + .llm-actions, .breadcrumbs { display: none !important; } + body { background: white; color: black; } + .llm-report { border: 1px solid #ccc; background: none; } } diff --git a/guarddog_nexus/web/templates/_llm_report_fragment.html b/guarddog_nexus/web/templates/_llm_report_fragment.html index 730bd53..b278d83 100644 --- a/guarddog_nexus/web/templates/_llm_report_fragment.html +++ b/guarddog_nexus/web/templates/_llm_report_fragment.html @@ -1,9 +1,10 @@ -
{{ report.summary }}
-{{ report.analysis }}
+{{ report.summary }}
+{{ report.analysis }}
No findings yet — scan results will appear here once packages are processed.
-{% endif %} - -{% if days %} -| Package | Version | Findings | Time |
|---|---|---|---|
| {{ s.package_name }} | +{{ s.package_version }} | +{{ s.total_findings }} | +{{ s.started_at.strftime('%m-%d %H:%M') if s.started_at }} | +
| Package | Version | Findings | |||
|---|---|---|---|---|---|
| Package | Version | Repo | Status | Time | |
| {{ p.package_name }} | -{{ p.package_version }} | -- {{ p.total }} - - | -
| Package | Version | Findings | Time | ||||
|---|---|---|---|---|---|---|---|
| {{ s.package_name }} | +{{ s.package_name }} | {{ s.package_version }} | -{{ s.total_findings }} | +{{ s.repository }} | ++ {% if s.status == 'scanning' %}scanning{% else %}{{ s.status }}{% endif %} + | +{% if s.flagged %}⚠ {{ s.total_findings }}{% elif s.status == 'completed' %}✓{% else %}-{% endif %} | {{ s.started_at.strftime('%m-%d %H:%M') if s.started_at }} |
| Package | -Version | -Repo | -Status | -- | Time | -
|---|---|---|---|---|---|
| {{ s.package_name }} | -{{ s.package_version }} | -{{ s.repository }} | -- {% if s.status == 'scanning' %}scanning{% else %}{{ s.status }}{% endif %} - | -{% if s.flagged %}⚠ {{ s.total_findings }}{% elif s.status == 'completed' %}✓{% else %}-{% endif %} | -{{ s.started_at.strftime('%m-%d %H:%M') if s.started_at }} | -
| ID | Repo | Status | Findings | Time |
|---|---|---|---|---|
| #{{ s.id }} | -{{ s.repository }} | -- {% if s.status == 'scanning' %}scanning{% else %}{{ s.status }}{% endif %} - | -{% if s.flagged %}{{ s.total_findings }}{% else %}0{% endif %} | -{{ s.started_at.strftime('%Y-%m-%d %H:%M') if s.started_at }} | -
| ID | Repo | Status | Findings | Time |
|---|---|---|---|---|
| #{{ s.id }} | +{{ s.repository }} | ++ {% if s.status == 'scanning' %}scanning{% else %}{{ s.status }}{% endif %} + | +{% if s.flagged %}{{ s.total_findings }}{% else %}0{% endif %} | +{{ s.started_at.strftime('%Y-%m-%d %H:%M') if s.started_at }} | +
{{ f.data.message }}
{% if f.data.code %} {% endfor %}| Package | {{ scan.package_name }} |
| Version | {{ scan.package_version }} |
| Ecosystem | {{ scan.ecosystem }} |
| Repository | {{ scan.repository }} |
| Status | - {% if scan.status == 'scanning' %}scanning{% else %}{{ scan.status }}{% endif %} - |
| SHA256 | {{ scan.sha256 or '-' }} |
| Started | {{ scan.started_at.isoformat() if scan.started_at }} |
| Finished | {{ scan.finished_at.isoformat() if scan.finished_at }} |
| Error | {{ scan.error_message }} |
{{ scan.sha256 or '-' }}{{ f.data.message }}
{% if f.data.code %} {% endfor %}