diff --git a/frontend/app.js b/frontend/app.js index 70d1d1a..9f5f741 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -21,6 +21,7 @@ import { showVulnDetail, exportVulnDetailHtml, exportVulnDetailMarkdown } from " import { showRepoDetail } from "./js/details/repoDetail.js"; import { closeDetail } from "./js/details/panel.js"; import { showTeamDetail, switchToTeams, setTeamSort, setTeamFilter, toggleTeamCheckRow, filterVulnsByTeam, filterUnownedRepos, showTeamAdmin } from "./js/views/teams.js"; +import { backFromCheckDetail } from "./js/views/checkDetail.js"; import { exportTeamReport } from "./js/utils/download.js"; // --------------------------------------------------------------------------- @@ -45,6 +46,7 @@ window.filterVulnsByTeam = filterVulnsByTeam; window.showTeamAdmin = showTeamAdmin; window.exportTeamReport = exportTeamReport; window.filterUnownedRepos = filterUnownedRepos; +window.backFromCheckDetail = backFromCheckDetail; window.downloadTeamMapping = () => downloadTeamMappingJson(); window.saveTeamMeta = function(teamId) { const slackInput = document.getElementById("admin-slack"); diff --git a/frontend/css/checkDetail.css b/frontend/css/checkDetail.css new file mode 100644 index 0000000..931a33b --- /dev/null +++ b/frontend/css/checkDetail.css @@ -0,0 +1,147 @@ +/* ================================================================ + Sjekk-detaljvisning + ================================================================ */ + +/* ── Header ── */ +.check-detail-header { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 1.25rem 1.5rem; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + margin-bottom: 1.5rem; +} + +.back-btn { + background: none; + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--text-muted); + cursor: pointer; + font-size: 0.85rem; + padding: 0.35rem 0.75rem; + transition: color var(--transition), border-color var(--transition); + white-space: nowrap; +} +.back-btn:hover { + color: var(--text); + border-color: var(--border-light); +} + +.check-detail-icon { + font-size: 1.75rem; + line-height: 1; +} + +.check-detail-titles { + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.check-detail-title { + font-size: 1.25rem; + font-weight: 700; + color: var(--text); + margin: 0; +} + +.check-detail-subtitle { + display: flex; + align-items: center; + gap: 0.4rem; + margin: 0; +} + +/* ── Sections ── */ +.check-detail-sections { + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +/* Top border accent per seksjon */ +.check-detail-failing { + border-top: 3px solid var(--severity-critical); +} + +.check-detail-passing { + border-top: 3px solid var(--success); +} + +.check-detail-na { + border-top: 3px solid var(--border-light); +} + +/* ── Prosjektgruppe ── */ +.check-detail-project-group { + margin-bottom: 0.5rem; +} +.check-detail-project-group:last-child { + margin-bottom: 0; +} + +.check-detail-project-header { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0; + border-bottom: 1px solid var(--border); + margin-bottom: 0.25rem; +} + +.check-detail-project-count { + font-size: 0.78rem; + color: var(--text-muted); +} + +/* ── Repo-rad ── */ +.check-detail-repo-list { + display: flex; + flex-direction: column; + gap: 0.15rem; +} + +.check-detail-repo-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.5rem 0.75rem; + border-radius: var(--radius); + cursor: pointer; + transition: background var(--transition); +} + +.check-detail-repo-row:hover { + background: var(--bg-card-hover); +} + +.check-detail-repo-left { + display: flex; + flex-direction: column; + gap: 0.2rem; + min-width: 0; +} + +.check-detail-repo-right { + flex-shrink: 0; + margin-left: 1rem; +} + +.check-detail-assessment { + font-size: 0.78rem; + color: var(--text-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 60vw; +} + +.check-detail-empty { + color: var(--text-muted); + font-size: 0.9rem; + padding: 0.5rem 0; + margin: 0; +} diff --git a/frontend/css/dashboard.css b/frontend/css/dashboard.css index ba1643f..9f8cd33 100644 --- a/frontend/css/dashboard.css +++ b/frontend/css/dashboard.css @@ -117,6 +117,14 @@ grid-template-columns: 180px 1fr 60px; align-items: center; gap: 0.75rem; + border-radius: var(--radius); + padding: 0.25rem 0.4rem; + margin: 0 -0.4rem; + transition: background var(--transition); +} + +.coverage-row:hover { + background: var(--bg-card-hover); } .coverage-label { diff --git a/frontend/index.html b/frontend/index.html index 1160924..9cf0df7 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -337,6 +337,11 @@

Team

+ + + diff --git a/frontend/js/state.js b/frontend/js/state.js index 3bc8f2f..5ac0a6c 100644 --- a/frontend/js/state.js +++ b/frontend/js/state.js @@ -23,6 +23,8 @@ export const state = { teamsConfig: null, /** Aktiv team-ID for detaljvisning (null = team-liste). */ activeTeam: null, + /** Aktiv sjekk-ID for sjekk-detaljvisning (null = ingen). */ + activeCheck: null, /** Filtre og sortering i Teams-fanen. */ teamFilters: { sortBy: "score", // "score" | "name" | "repos" diff --git a/frontend/js/views/checkDetail.js b/frontend/js/views/checkDetail.js new file mode 100644 index 0000000..508bba2 --- /dev/null +++ b/frontend/js/views/checkDetail.js @@ -0,0 +1,150 @@ +/* ================================================================ + Argus Frontend — Sjekk-detaljvisning + Viser alle repos fordelt på "avvik" og "består", sortert + etter Bitbucket-prosjekt, deretter repo. + ================================================================ */ +"use strict"; + +import { state } from "../state.js"; +import { $, escapeHtml } from "../utils/dom.js"; +import { CHECK_LABELS, CHECK_ICONS } from "../constants/checkLabels.js"; +import { assessmentLevel } from "../utils/assessment.js"; +import { switchView } from "./router.js"; + +export function renderCheckDetail() { + const checkId = state.activeCheck; + const container = $("#check-detail-container"); + + if (!checkId || !state.report) { + switchView("summary"); + return; + } + + const report = state.report; + const label = CHECK_LABELS[checkId] || checkId; + const icon = CHECK_ICONS[checkId] || "📋"; + + // Sorter repos: prosjekt stigende, deretter repo stigende + const allRepos = [...report.repos].sort((a, b) => { + const proj = a.project.localeCompare(b.project, "nb"); + return proj !== 0 ? proj : a.repo.localeCompare(b.repo, "nb"); + }); + + const failing = allRepos.filter(r => { + const level = assessmentLevel(r, checkId); + return level === "action" || level === "fail" || level === "unknown"; + }); + + const passing = allRepos.filter(r => assessmentLevel(r, checkId) === "pass"); + const na = allRepos.filter(r => assessmentLevel(r, checkId) === "na"); + + container.innerHTML = ` +
+ + ${icon} +
+

${escapeHtml(label)}

+

+ ${failing.length} avvik + ${passing.length} består + ${na.length > 0 ? `${na.length} ikke aktuelt` : ""} +

+
+
+ +
+ ${renderRepoSection(failing, checkId, "avvik", "check-detail-failing")} + ${renderRepoSection(passing, checkId, "består", "check-detail-passing")} + ${na.length > 0 ? renderRepoSection(na, checkId, "ikke aktuelt", "check-detail-na") : ""} +
+ `; +} + +function renderRepoSection(repos, checkId, sectionLabel, sectionClass) { + const isEmpty = repos.length === 0; + const isFailing = sectionClass === "check-detail-failing"; + const isPassing = sectionClass === "check-detail-passing"; + + const title = isFailing + ? `Repos med avvik (${repos.length})` + : isPassing + ? `Repos som består (${repos.length})` + : `Ikke aktuelt (${repos.length})`; + + if (isEmpty) { + const emptyMsg = isFailing + ? "✅ Ingen repos med avvik — alle er på stell!" + : "Ingen repos."; + return ` +
+

${escapeHtml(title)}

+

${emptyMsg}

+
+ `; + } + + // Grupper etter prosjekt + const byProject = {}; + for (const repo of repos) { + if (!byProject[repo.project]) byProject[repo.project] = []; + byProject[repo.project].push(repo); + } + + const projectBlocks = Object.keys(byProject).sort((a, b) => a.localeCompare(b, "nb")).map(project => { + const repoRows = byProject[project].map(repo => buildRepoRow(repo, checkId, isFailing)).join(""); + return ` +
+
+ ${escapeHtml(project)} + ${byProject[project].length} repo${byProject[project].length !== 1 ? "s" : ""} +
+
${repoRows}
+
+ `; + }).join(""); + + return ` +
+

${escapeHtml(title)}

+ ${projectBlocks} +
+ `; +} + +function buildRepoRow(repo, checkId, showAssessment) { + const level = assessmentLevel(repo, checkId); + const assessment = repo.assessments?.[checkId] || ""; + + let levelBadge = ""; + if (level === "action") { + levelBadge = 'Tiltak'; + } else if (level === "fail" || level === "unknown") { + levelBadge = 'Avvik'; + } else if (level === "pass") { + levelBadge = ''; + } else { + levelBadge = ''; + } + + const assessmentHtml = showAssessment && assessment + ? `${escapeHtml(assessment)}` + : ""; + + return ` +
+
+ ${escapeHtml(repo.repo)} + ${assessmentHtml} +
+
+ ${levelBadge} +
+
+ `; +} + +/** Naviger tilbake til sammendrag-visningen. */ +export function backFromCheckDetail() { + state.activeCheck = null; + switchView("summary"); +} diff --git a/frontend/js/views/repos.js b/frontend/js/views/repos.js index 3f62d43..db22092 100644 --- a/frontend/js/views/repos.js +++ b/frontend/js/views/repos.js @@ -136,12 +136,8 @@ export function filterByProject(project) { switchView("vulnerabilities"); } -/** Drill-down fra sjekk-breakdown — bytter til riktig fane. */ +/** Drill-down fra sjekk-breakdown — åpner sjekk-detaljsiden. */ export function filterByCheck(checkId) { - if (checkId === "dep-vulns") { - state.vulnFilters = { severity: [], ecosystem: [], projects: [], fixAvailable: [], team: [] }; - switchView("vulnerabilities"); - } else { - switchView("repos"); - } + state.activeCheck = checkId; + switchView("check-detail"); } diff --git a/frontend/js/views/router.js b/frontend/js/views/router.js index 35a2d07..d925b1c 100644 --- a/frontend/js/views/router.js +++ b/frontend/js/views/router.js @@ -9,6 +9,7 @@ import { renderSummary } from "./summary.js"; import { renderExplorer } from "./vulnerabilities.js"; import { renderRepos } from "./repos.js"; import { renderTeamList, renderTeamDetail, renderTeamAdmin } from "./teams.js"; +import { renderCheckDetail } from "./checkDetail.js"; /** Bytt aktiv visning og rendre den. */ export function switchView(view) { @@ -44,5 +45,9 @@ export function renderActiveView() { if (!state.hasTeams || !state.activeTeam) { switchView("teams"); return; } renderTeamAdmin(state.activeTeam); break; + case "check-detail": + if (!state.activeCheck) { switchView("summary"); return; } + renderCheckDetail(); + break; } } diff --git a/frontend/js/views/summary.js b/frontend/js/views/summary.js index 08bca2f..a5539a6 100644 --- a/frontend/js/views/summary.js +++ b/frontend/js/views/summary.js @@ -85,7 +85,7 @@ function renderCoverageChart() { const label = CHECK_LABELS[checkId] || checkId; html += ` -
+
${escapeHtml(label)}
diff --git a/frontend/styles.css b/frontend/styles.css index caea111..389f722 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -16,4 +16,5 @@ @import url("./css/modal.css"); @import url("./css/toast.css"); @import url("./css/docs.css"); +@import url("./css/checkDetail.css"); @import url("./css/responsive.css");