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 = `
+
+
+
+ ${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 `
+
+ `;
+ }).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");