You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2026/08/28 10:22:01 UTC

(maven) 06/07: introduced mvnlog

This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch feature/mvnlog-web
in repository https://gitbox.apache.org/repos/asf/maven.git

commit 08becba1332086a6269da9e41cd1931ae591f664
Author: sakshi8778 <sa...@gmail.com>
AuthorDate: Sun Aug 9 23:51:41 2026 +0530

     introduced mvnlog
---
 apache-maven/src/assembly/component.xml            |    1 +
 apache-maven/src/assembly/maven/bin/mvn.cmd        |    1 +
 .../apache/maven/api/cli/mvnlog/package-info.java  |   34 +-
 .../apache/maven/cling/invoker/LookupInvoker.java  |    7 +-
 .../apache/maven/cling/invoker/mvnlog/report.html  | 1108 ++++++++++++++++++++
 .../apache/maven/logging/BuildEventListener.java   |    5 +-
 .../maven/logging/BuildReportEventListener.java    |  452 ++++++++
 .../maven/logging/SimpleBuildEventListener.java    |    6 +-
 8 files changed, 1583 insertions(+), 31 deletions(-)

diff --git a/apache-maven/src/assembly/component.xml b/apache-maven/src/assembly/component.xml
index ce303ea496..60d791f348 100644
--- a/apache-maven/src/assembly/component.xml
+++ b/apache-maven/src/assembly/component.xml
@@ -90,6 +90,7 @@ under the License.
         <include>mvnlog</include>
         <include>mvnsh</include>
         <include>mvnup</include>
+        <include>mvnlog</include>
         <include>mvnDebug</include>
         <include>mvnencDebug</include>
         <!-- This is so that CI systems can periodically run the profiler -->
diff --git a/apache-maven/src/assembly/maven/bin/mvn.cmd b/apache-maven/src/assembly/maven/bin/mvn.cmd
index 7e3c8bdae8..427400313c 100644
--- a/apache-maven/src/assembly/maven/bin/mvn.cmd
+++ b/apache-maven/src/assembly/maven/bin/mvn.cmd
@@ -278,6 +278,7 @@ if "%~1"=="--debug" (
 ) else if "%~1"=="--log" (
       set "MAVEN_MAIN_CLASS=org.apache.maven.cling.MavenLogCling"
 )
+
 exit /b 0
 
 :processArgs
diff --git a/impl/maven-core/src/main/java/org/apache/maven/logging/BuildEventListener.java b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnlog/package-info.java
similarity index 51%
copy from impl/maven-core/src/main/java/org/apache/maven/logging/BuildEventListener.java
copy to api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnlog/package-info.java
index c1d771b5c1..3fd00c3e4e 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/logging/BuildEventListener.java
+++ b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnlog/package-info.java
@@ -16,34 +16,12 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.maven.logging;
-
-import org.apache.maven.api.build.report.LogEvent;
-import org.apache.maven.execution.ExecutionEvent;
-import org.eclipse.aether.transfer.TransferEvent;
 
 /**
- * An abstract build event sink.
+ * Provides the API for the Maven Log Viewer tool ({@code mvnlog}).
+ *
+ * @see org.apache.maven.api.cli.Tools#MVNLOG_CMD
+ * @see org.apache.maven.api.cli.Tools#MVNLOG_NAME
+ * @since 4.0.0
  */
-public interface BuildEventListener {
-
-    void sessionStarted(ExecutionEvent event);
-
-    void projectStarted(String projectId);
-
-    void projectLogMessage(String projectId, LogEvent event);
-
-    void projectFinished(String projectId);
-
-    void executionFailure(String projectId, boolean halted, String exception);
-
-    void mojoStarted(ExecutionEvent event);
-
-    void finish(int exitCode) throws Exception;
-
-    void fail(Throwable t) throws Exception;
-
-    void log(String msg);
-
-    void transfer(String projectId, TransferEvent e);
-}
+package org.apache.maven.api.cli.mvnlog;
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java
index 4a6fe8f18c..aeb038a573 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java
@@ -335,9 +335,14 @@ protected BuildEventListener determineBuildEventListener(C context) {
 
     protected BuildEventListener doDetermineBuildEventListener(C context) {
         Consumer<String> writer = determineWriter(context);
-        return new SimpleBuildEventListener(writer);
+        BuildEventListener delegate = new SimpleBuildEventListener(writer);
+        if (context.invokerRequest.command().equals("mvn")) {
+            return new org.apache.maven.logging.BuildReportEventListener(delegate);
+        }
+        return delegate;
     }
 
+
     protected final void createTerminal(C context) {
         if (context.terminal == null) {
             MessageUtils.systemInstall(builder -> doCreateTerminal(context, builder), terminal -> {
diff --git a/impl/maven-cli/src/main/resources/org/apache/maven/cling/invoker/mvnlog/report.html b/impl/maven-cli/src/main/resources/org/apache/maven/cling/invoker/mvnlog/report.html
new file mode 100644
index 0000000000..78726e7403
--- /dev/null
+++ b/impl/maven-cli/src/main/resources/org/apache/maven/cling/invoker/mvnlog/report.html
@@ -0,0 +1,1108 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <title>Maven Build Report Explorer</title>
+  <link rel="preconnect" href="https://fonts.googleapis.com">
+  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
+  <style>
+    :root {
+      --bg-primary: #0f172a;
+      --bg-secondary: #1e293b;
+      --bg-tertiary: #334155;
+      --text-primary: #f8fafc;
+      --text-secondary: #94a3b8;
+      --accent-color: #3b82f6;
+      --accent-hover: #2563eb;
+      --success-color: #10b981;
+      --success-bg: rgba(16, 185, 129, 0.15);
+      --failure-color: #ef4444;
+      --failure-bg: rgba(239, 68, 68, 0.15);
+      --warning-color: #f59e0b;
+      --warning-bg: rgba(245, 158, 11, 0.15);
+      --info-color: #06b6d4;
+      --border-color: #334155;
+      --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+      --font-mono: 'JetBrains Mono', monospace;
+    }
+
+    @media (prefers-color-scheme: light) {
+      :root {
+        --bg-primary: #f8fafc;
+        --bg-secondary: #ffffff;
+        --bg-tertiary: #f1f5f9;
+        --text-primary: #0f172a;
+        --text-secondary: #64748b;
+        --accent-color: #2563eb;
+        --accent-hover: #1d4ed8;
+        --success-color: #059669;
+        --success-bg: rgba(5, 150, 105, 0.1);
+        --failure-color: #dc2626;
+        --failure-bg: rgba(220, 38, 38, 0.1);
+        --warning-color: #d97706;
+        --warning-bg: rgba(217, 119, 6, 0.1);
+        --info-color: #0891b2;
+        --border-color: #cbd5e1;
+      }
+    }
+
+    * {
+      box-sizing: border-box;
+      margin: 0;
+      padding: 0;
+    }
+
+    body {
+      background-color: var(--bg-primary);
+      color: var(--text-primary);
+      font-family: var(--font-sans);
+      height: 100vh;
+      overflow: hidden;
+      display: flex;
+    }
+
+    /* Sidebar Styles */
+    .sidebar {
+      width: 280px;
+      background-color: var(--bg-secondary);
+      border-right: 1px solid var(--border-color);
+      display: flex;
+      flex-direction: flex-direction;
+      flex-direction: column;
+      flex-shrink: 0;
+    }
+
+    .sidebar-header {
+      padding: 20px;
+      border-bottom: 1px solid var(--border-color);
+      display: flex;
+      align-items: center;
+      gap: 10px;
+    }
+
+    .sidebar-header h2 {
+      font-size: 1.1rem;
+      font-weight: 700;
+      letter-spacing: -0.025em;
+    }
+
+    .report-list {
+      flex: 1;
+      overflow-y: auto;
+      padding: 10px;
+    }
+
+    .report-item {
+      padding: 12px;
+      border-radius: 8px;
+      cursor: pointer;
+      margin-bottom: 8px;
+      transition: all 0.2s ease;
+      border: 1px solid transparent;
+    }
+
+    .report-item:hover {
+      background-color: var(--bg-tertiary);
+    }
+
+    .report-item.active {
+      background-color: var(--bg-tertiary);
+      border-color: var(--accent-color);
+    }
+
+    .report-item-title {
+      font-weight: 500;
+      font-size: 0.85rem;
+      margin-bottom: 4px;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+
+    .report-item-meta {
+      display: flex;
+      justify-content: space-between;
+      font-size: 0.75rem;
+      color: var(--text-secondary);
+    }
+
+    .badge {
+      padding: 2px 6px;
+      border-radius: 4px;
+      font-weight: 600;
+      font-size: 0.7rem;
+      text-transform: uppercase;
+    }
+
+    .badge-success {
+      background-color: var(--success-bg);
+      color: var(--success-color);
+    }
+
+    .badge-failure {
+      background-color: var(--failure-bg);
+      color: var(--failure-color);
+    }
+
+    /* Main Area Styles */
+    .main-content {
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+      height: 100%;
+      overflow: hidden;
+    }
+
+    /* Header Panel */
+    .header-panel {
+      background-color: var(--bg-secondary);
+      border-bottom: 1px solid var(--border-color);
+      padding: 20px 30px;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      flex-shrink: 0;
+    }
+
+    .build-title-area {
+      display: flex;
+      align-items: center;
+      gap: 15px;
+    }
+
+    .build-title-area h1 {
+      font-size: 1.4rem;
+      font-weight: 700;
+      letter-spacing: -0.025em;
+    }
+
+    .build-meta-grid {
+      display: flex;
+      gap: 20px;
+      font-size: 0.85rem;
+      color: var(--text-secondary);
+      margin-top: 6px;
+    }
+
+    .build-meta-grid span strong {
+      color: var(--text-primary);
+    }
+
+    /* Tabs Navigation */
+    .tabs-nav {
+      display: flex;
+      gap: 5px;
+      background-color: var(--bg-secondary);
+      border-bottom: 1px solid var(--border-color);
+      padding: 0 30px;
+      flex-shrink: 0;
+    }
+
+    .tab-btn {
+      padding: 14px 20px;
+      background: none;
+      border: none;
+      color: var(--text-secondary);
+      font-weight: 500;
+      font-size: 0.9rem;
+      cursor: pointer;
+      border-bottom: 2px solid transparent;
+      transition: all 0.2s ease;
+    }
+
+    .tab-btn:hover {
+      color: var(--text-primary);
+    }
+
+    .tab-btn.active {
+      color: var(--accent-color);
+      border-bottom-color: var(--accent-color);
+    }
+
+    /* Panels Layout */
+    .panels-container {
+      flex: 1;
+      overflow: hidden;
+      position: relative;
+    }
+
+    .panel {
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      display: none;
+      flex-direction: column;
+      padding: 30px;
+      overflow-y: auto;
+    }
+
+    .panel.active {
+      display: flex;
+    }
+
+    /* Dashboard Layout */
+    .dashboard-grid {
+      display: grid;
+      grid-template-columns: 2fr 1fr;
+      gap: 30px;
+      height: 100%;
+      min-height: 0;
+    }
+
+    .dashboard-left {
+      display: flex;
+      flex-direction: column;
+      gap: 20px;
+      min-height: 0;
+      overflow-y: auto;
+    }
+
+    .card {
+      background-color: var(--bg-secondary);
+      border-radius: 12px;
+      border: 1px solid var(--border-color);
+      padding: 24px;
+      display: flex;
+      flex-direction: column;
+    }
+
+    .card-title {
+      font-size: 1rem;
+      font-weight: 600;
+      margin-bottom: 16px;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+    }
+
+    /* Gantt Chart */
+    .gantt-chart {
+      display: flex;
+      flex-direction: column;
+      gap: 12px;
+      margin-top: 10px;
+    }
+
+    .gantt-row {
+      display: grid;
+      grid-template-columns: 180px 1fr;
+      align-items: center;
+      gap: 15px;
+      cursor: pointer;
+      padding: 4px;
+      border-radius: 6px;
+      transition: background 0.2s;
+    }
+
+    .gantt-row:hover {
+      background-color: var(--bg-tertiary);
+    }
+
+    .gantt-label {
+      font-size: 0.85rem;
+      font-weight: 500;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+
+    .gantt-track {
+      height: 24px;
+      background-color: var(--bg-tertiary);
+      border-radius: 4px;
+      position: relative;
+      overflow: hidden;
+    }
+
+    .gantt-bar {
+      height: 100%;
+      border-radius: 4px;
+      position: absolute;
+      min-width: 4px;
+      display: flex;
+      align-items: center;
+      padding-left: 8px;
+      font-size: 0.7rem;
+      color: #fff;
+      font-weight: 600;
+      text-shadow: 0 1px 2px rgba(0,0,0,0.5);
+    }
+
+    .gantt-bar-success {
+      background: linear-gradient(90deg, #10b981, #059669);
+    }
+
+    .gantt-bar-failed {
+      background: linear-gradient(90deg, #ef4444, #dc2626);
+    }
+
+    .gantt-bar-skipped {
+      background: linear-gradient(90deg, #94a3b8, #64748b);
+    }
+
+    /* Mojo Breakdown */
+    .mojo-detail-panel {
+      overflow-y: auto;
+    }
+
+    .mojo-list {
+      display: flex;
+      flex-direction: column;
+      gap: 10px;
+    }
+
+    .mojo-item {
+      background-color: var(--bg-tertiary);
+      border-radius: 8px;
+      padding: 14px;
+      border-left: 4px solid var(--accent-color);
+    }
+
+    .mojo-item.failed {
+      border-left-color: var(--failure-color);
+    }
+
+    .mojo-header {
+      display: flex;
+      justify-content: space-between;
+      font-size: 0.85rem;
+      font-weight: 600;
+      margin-bottom: 6px;
+    }
+
+    .mojo-meta {
+      font-size: 0.75rem;
+      color: var(--text-secondary);
+      display: flex;
+      gap: 15px;
+    }
+
+    .mojo-logs-toggle {
+      background: none;
+      border: none;
+      color: var(--accent-color);
+      font-size: 0.75rem;
+      cursor: pointer;
+      margin-top: 8px;
+      display: inline-flex;
+      align-items: center;
+      gap: 4px;
+    }
+
+    .mojo-logs-toggle:hover {
+      text-decoration: underline;
+    }
+
+    .mojo-logs {
+      display: none;
+      background-color: var(--bg-primary);
+      padding: 10px;
+      border-radius: 6px;
+      margin-top: 8px;
+      font-family: var(--font-mono);
+      font-size: 0.75rem;
+      max-height: 200px;
+      overflow-y: auto;
+      white-space: pre-wrap;
+    }
+
+    /* Problems Panel Styles */
+    .problems-filters {
+      display: flex;
+      gap: 15px;
+      margin-bottom: 20px;
+      align-items: center;
+    }
+
+    .search-input {
+      background-color: var(--bg-secondary);
+      border: 1px solid var(--border-color);
+      color: var(--text-primary);
+      padding: 8px 14px;
+      border-radius: 6px;
+      font-size: 0.85rem;
+      flex: 1;
+    }
+
+    .search-input:focus {
+      outline: 1px solid var(--accent-color);
+    }
+
+    .filter-select {
+      background-color: var(--bg-secondary);
+      border: 1px solid var(--border-color);
+      color: var(--text-primary);
+      padding: 8px 14px;
+      border-radius: 6px;
+      font-size: 0.85rem;
+    }
+
+    .problem-table {
+      width: 100%;
+      border-collapse: collapse;
+      background-color: var(--bg-secondary);
+      border-radius: 8px;
+      overflow: hidden;
+      border: 1px solid var(--border-color);
+    }
+
+    .problem-table th, .problem-table td {
+      padding: 14px 20px;
+      text-align: left;
+      border-bottom: 1px solid var(--border-color);
+    }
+
+    .problem-table th {
+      background-color: var(--bg-tertiary);
+      font-size: 0.8rem;
+      text-transform: uppercase;
+      font-weight: 600;
+      color: var(--text-secondary);
+      cursor: pointer;
+    }
+
+    .problem-table tr:last-child td {
+      border-bottom: none;
+    }
+
+    .problem-table tr:hover td {
+      background-color: rgba(255,255,255,0.02);
+    }
+
+    /* Failures Panel */
+    .failures-list {
+      display: flex;
+      flex-direction: column;
+      gap: 20px;
+    }
+
+    .failure-item {
+      background-color: var(--bg-secondary);
+      border: 1px solid var(--border-color);
+      border-radius: 12px;
+      padding: 20px;
+    }
+
+    .failure-header {
+      font-weight: 600;
+      margin-bottom: 12px;
+      color: var(--failure-color);
+      display: flex;
+      gap: 10px;
+      align-items: center;
+    }
+
+    .failure-stack {
+      background-color: var(--bg-primary);
+      padding: 15px;
+      border-radius: 8px;
+      font-family: var(--font-mono);
+      font-size: 0.8rem;
+      line-height: 1.5;
+      overflow-x: auto;
+      max-height: 400px;
+      border: 1px solid var(--border-color);
+    }
+
+    /* Log Viewer Panel */
+    .log-viewer-container {
+      display: flex;
+      flex-direction: column;
+      flex: 1;
+      height: 100%;
+      background-color: var(--bg-secondary);
+      border-radius: 12px;
+      border: 1px solid var(--border-color);
+      overflow: hidden;
+    }
+
+    .log-toolbar {
+      padding: 15px 20px;
+      border-bottom: 1px solid var(--border-color);
+      display: flex;
+      gap: 15px;
+      align-items: center;
+      background-color: var(--bg-tertiary);
+    }
+
+    .log-lines-window {
+      flex: 1;
+      overflow-y: auto;
+      font-family: var(--font-mono);
+      font-size: 0.8rem;
+      line-height: 1.6;
+      background-color: var(--bg-primary);
+      position: relative;
+    }
+
+    .log-line {
+      padding: 2px 20px;
+      white-space: pre-wrap;
+    }
+
+    .log-line.level-ERROR {
+      color: #f87171;
+      background-color: rgba(239, 68, 68, 0.05);
+    }
+
+    .log-line.level-WARN {
+      color: #fbbf24;
+      background-color: rgba(245, 158, 11, 0.05);
+    }
+
+    .log-line.level-DEBUG {
+      color: #64748b;
+    }
+
+    .highlight-search {
+      background-color: rgba(245, 158, 11, 0.4);
+      color: #fff;
+      font-weight: bold;
+    }
+  </style>
+</head>
+<body>
+
+  <!-- Sidebar -->
+  <div class="sidebar">
+    <div class="sidebar-header">
+      <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-activity"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg>
+      <h2>Build Reports</h2>
+    </div>
+    <div class="report-list" id="reportList">
+      <!-- Dyn list -->
+    </div>
+  </div>
+
+  <!-- Main Content -->
+  <div class="main-content">
+    <!-- Header -->
+    <div class="header-panel">
+      <div>
+        <div class="build-title-area">
+          <h1 id="headerProjectName">Loading Project...</h1>
+          <span class="badge" id="headerStatusBadge">SUCCESS</span>
+        </div>
+        <div class="build-meta-grid">
+          <span>Goals: <strong id="headerGoals">-</strong></span>
+          <span>Duration: <strong id="headerDuration">-</strong></span>
+          <span>Threads: <strong id="headerThreads">-</strong></span>
+        </div>
+      </div>
+      <div style="font-size: 0.8rem; color: var(--text-secondary); text-align: right;">
+        <div>Maven: <span id="headerMavenVer" style="color: var(--text-primary)">-</span></div>
+        <div>Java: <span id="headerJavaVer" style="color: var(--text-primary)">-</span></div>
+      </div>
+    </div>
+
+    <!-- Tabs Nav -->
+    <div class="tabs-nav">
+      <button class="tab-btn active" onclick="switchTab('dashboard')">Dashboard</button>
+      <button class="tab-btn" onclick="switchTab('problems')">Problems <span id="problemsCountBadge"></span></button>
+      <button class="tab-btn" onclick="switchTab('failures')">Failures <span id="failuresCountBadge"></span></button>
+      <button class="tab-btn" onclick="switchTab('logs')">Full Build Log</button>
+    </div>
+
+    <!-- Panels Container -->
+    <div class="panels-container">
+      <!-- Dashboard -->
+      <div class="panel active" id="dashboardPanel">
+        <div class="dashboard-grid">
+          <!-- Timeline Card -->
+          <div class="card">
+            <div class="card-title">Module Build Timeline (Gantt Chart)</div>
+            <div class="gantt-chart" id="ganttChart">
+              <!-- Dyn Gantt -->
+            </div>
+          </div>
+          <!-- Mojo Details -->
+          <div class="card mojo-detail-panel">
+            <div class="card-title" id="mojoPanelTitle">Mojo Executions</div>
+            <div class="mojo-list" id="mojoList">
+              <div style="color: var(--text-secondary); text-align: center; margin-top: 40px;">Click a module in the timeline to view its execution details.</div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- Problems -->
+      <div class="panel" id="problemsPanel">
+        <div class="problems-filters">
+          <input type="text" class="search-input" placeholder="Search problems..." id="problemSearch" oninput="filterProblems()">
+          <select class="filter-select" id="problemSeverityFilter" onchange="filterProblems()">
+            <option value="ALL">All Severities</option>
+            <option value="ERROR">Errors Only</option>
+            <option value="WARN">Warnings Only</option>
+          </select>
+        </div>
+        <table class="problem-table">
+          <thead>
+            <tr>
+              <th onclick="sortProblems('severity')">Severity</th>
+              <th onclick="sortProblems('source')">Source</th>
+              <th onclick="sortProblems('key')">Key</th>
+              <th>Message</th>
+              <th>Suggestion</th>
+            </tr>
+          </thead>
+          <tbody id="problemsTableBody">
+            <!-- Dyn items -->
+          </tbody>
+        </table>
+      </div>
+
+      <!-- Failures -->
+      <div class="panel" id="failuresPanel">
+        <div class="failures-list" id="failuresList">
+          <!-- Dyn items -->
+        </div>
+      </div>
+
+      <!-- Full Log Viewer -->
+      <div class="panel" id="logsPanel" style="padding: 20px;">
+        <div class="log-viewer-container">
+          <div class="log-toolbar">
+            <input type="text" class="search-input" placeholder="Search log lines..." id="logSearch" oninput="filterLogs()">
+            <select class="filter-select" id="logModuleFilter" onchange="filterLogs()">
+              <option value="ALL">All Modules</option>
+            </select>
+            <select class="filter-select" id="logLevelFilter" onchange="filterLogs()">
+              <option value="ALL">All Levels</option>
+              <option value="ERROR">ERROR</option>
+              <option value="WARN">WARN</option>
+              <option value="INFO">INFO</option>
+              <option value="DEBUG">DEBUG</option>
+            </select>
+          </div>
+          <div class="log-lines-window" id="logLinesWindow">
+            <div id="logLinesScrollHeight" style="position: relative;"></div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <script>
+    let reportData = null;
+    let currentTab = 'dashboard';
+    let selectedModuleId = null;
+
+    // Problems sorting state
+    let problemsSortField = 'severity';
+    let problemsSortAsc = true;
+
+    // Fetch and load reports list
+    async function loadReportsList() {
+      try {
+        const res = await fetch('/api/reports');
+        const list = await res.json();
+        const container = document.getElementById('reportList');
+        container.innerHTML = '';
+        list.forEach((r, idx) => {
+          const div = document.createElement('div');
+          div.className = `report-item ${idx === 0 ? 'active' : ''}`;
+          div.onclick = () => {
+            document.querySelectorAll('.report-item').forEach(el => el.classList.remove('active'));
+            div.classList.add('active');
+            loadReport(r.id);
+          };
+
+          const title = document.createElement('div');
+          title.className = 'report-item-title';
+          title.innerText = r.id;
+
+          const meta = document.createElement('div');
+          meta.className = 'report-item-meta';
+          
+          const badge = document.createElement('span');
+          badge.className = `badge ${r.status === 'SUCCESS' ? 'badge-success' : 'badge-failure'}`;
+          badge.innerText = r.status;
+
+          const time = document.createElement('span');
+          time.innerText = formatDuration(r.durationMs);
+
+          meta.appendChild(badge);
+          meta.appendChild(time);
+          div.appendChild(title);
+          div.appendChild(meta);
+          container.appendChild(div);
+        });
+
+        if (list.length > 0) {
+          loadReport(list[0].id);
+        }
+      } catch (e) {
+        console.error('Failed to load reports list', e);
+      }
+    }
+
+    async function loadReport(id) {
+      try {
+        const res = await fetch(`/api/report?id=${id}`);
+        reportData = await res.json();
+        renderReport();
+      } catch (e) {
+        console.error('Failed to load report data', e);
+      }
+    }
+
+    function renderReport() {
+      // Header
+      const projectName = reportData.modules.length > 0 ? reportData.modules[reportData.modules.length - 1].id : "Root Project";
+      document.getElementById('headerProjectName').innerText = projectName;
+      
+      const statusBadge = document.getElementById('headerStatusBadge');
+      statusBadge.innerText = reportData.metadata.status;
+      statusBadge.className = `badge ${reportData.metadata.status === 'SUCCESS' ? 'badge-success' : 'badge-failure'}`;
+
+      document.getElementById('headerGoals').innerText = reportData.metadata.goals.join(', ') || 'none';
+      document.getElementById('headerDuration').innerText = formatDuration(reportData.metadata.durationMs);
+      document.getElementById('headerThreads').innerText = reportData.metadata.threadCount;
+      document.getElementById('headerMavenVer').innerText = reportData.metadata.mavenVersion;
+      document.getElementById('headerJavaVer').innerText = reportData.metadata.javaVersion;
+
+      // Badges count
+      const warningsCount = reportData.problems.filter(p => p.severity === 'WARN').length;
+      const errorsCount = reportData.problems.filter(p => p.severity === 'ERROR').length;
+      document.getElementById('problemsCountBadge').innerText = warningsCount + errorsCount > 0 ? `(${warningsCount + errorsCount})` : '';
+      document.getElementById('failuresCountBadge').innerText = reportData.failures.length > 0 ? `(${reportData.failures.length})` : '';
+
+      // Populate log filters
+      const moduleFilter = document.getElementById('logModuleFilter');
+      moduleFilter.innerHTML = '<option value="ALL">All Modules</option>';
+      reportData.modules.forEach(m => {
+        const opt = document.createElement('option');
+        opt.value = m.id;
+        opt.innerText = m.id;
+        moduleFilter.appendChild(opt);
+      });
+
+      renderTimeline();
+      filterProblems();
+      renderFailures();
+      setupVirtualScroll();
+    }
+
+    function renderTimeline() {
+      const container = document.getElementById('ganttChart');
+      container.innerHTML = '';
+
+      if (reportData.modules.length === 0) return;
+
+      const buildStart = reportData.metadata.startTime;
+      const buildEnd = reportData.metadata.endTime;
+      const buildDur = buildEnd - buildStart || 1;
+
+      reportData.modules.forEach(m => {
+        const row = document.createElement('div');
+        row.className = 'gantt-row';
+        row.onclick = () => showMojoDetails(m.id);
+
+        const label = document.createElement('div');
+        label.className = 'gantt-label';
+        label.innerText = m.id;
+        label.title = m.id;
+
+        const track = document.createElement('div');
+        track.className = 'gantt-track';
+
+        const leftPct = ((m.startTime - buildStart) / buildDur) * 100;
+        const widthPct = ((m.endTime - m.startTime) / buildDur) * 100;
+
+        const bar = document.createElement('div');
+        bar.className = `gantt-bar gantt-bar-${m.status.toLowerCase()}`;
+        bar.style.left = `${Math.max(0, Math.min(leftPct, 100))}%`;
+        bar.style.width = `${Math.max(1, Math.min(widthPct, 100))}%`;
+        bar.innerText = formatDuration(m.endTime - m.startTime);
+
+        track.appendChild(bar);
+        row.appendChild(label);
+        row.appendChild(track);
+        container.appendChild(row);
+      });
+    }
+
+    function showMojoDetails(moduleId) {
+      selectedModuleId = moduleId;
+      const module = reportData.modules.find(m => m.id === moduleId);
+      const title = document.getElementById('mojoPanelTitle');
+      const list = document.getElementById('mojoList');
+
+      title.innerText = `Mojo Executions: ${moduleId}`;
+      list.innerHTML = '';
+
+      if (!module || module.mojos.length === 0) {
+        list.innerHTML = '<div style="color: var(--text-secondary); text-align: center; margin-top: 40px;">No mojo executions recorded for this module.</div>';
+        return;
+      }
+
+      module.mojos.forEach((mojo, idx) => {
+        const item = document.createElement('div');
+        item.className = `mojo-item ${mojo.status === 'FAILED' ? 'failed' : ''}`;
+
+        const header = document.createElement('div');
+        header.className = 'mojo-header';
+        header.innerHTML = `<span>${mojo.goal}</span><span style="color: var(--text-secondary); font-weight: 500">${formatDuration(mojo.endTime - mojo.startTime)}</span>`;
+
+        const meta = document.createElement('div');
+        meta.className = 'mojo-meta';
+        meta.innerHTML = `<span>Phase: <strong>${mojo.phase}</strong></span><span>Plugin: <strong>${mojo.pluginArtifactId}</strong></span>`;
+
+        item.appendChild(header);
+        item.appendChild(meta);
+
+        // Find mojo logs
+        const mojoId = `${mojo.pluginGroupId}:${mojo.pluginArtifactId}:${mojo.pluginVersion}:${mojo.goal}:${mojo.executionId}`;
+        const mojoLogs = reportData.logs.filter(l => l.mojoId === mojoId);
+        if (mojoLogs.length > 0) {
+          const toggle = document.createElement('button');
+          toggle.className = 'mojo-logs-toggle';
+          toggle.innerText = 'View Logs';
+          toggle.onclick = () => {
+            const logsBox = item.querySelector('.mojo-logs');
+            if (logsBox.style.display === 'block') {
+              logsBox.style.display = 'none';
+              toggle.innerText = 'View Logs';
+            } else {
+              logsBox.style.display = 'block';
+              toggle.innerText = 'Hide Logs';
+            }
+          };
+          item.appendChild(toggle);
+
+          const logsDiv = document.createElement('div');
+          logsDiv.className = 'mojo-logs';
+          logsDiv.innerText = mojoLogs.map(l => l.message).join('\n');
+          item.appendChild(logsDiv);
+        }
+
+        list.appendChild(item);
+      });
+    }
+
+    function filterProblems() {
+      if (!reportData) return;
+
+      const search = document.getElementById('problemSearch').value.toLowerCase();
+      const severity = document.getElementById('problemSeverityFilter').value;
+
+      let filtered = reportData.problems.filter(p => {
+        const matchesSearch = p.message.toLowerCase().includes(search) || p.key.toLowerCase().includes(search);
+        const matchesSeverity = severity === 'ALL' || p.severity === severity;
+        return matchesSearch && matchesSeverity;
+      });
+
+      // Sort
+      filtered.sort((a, b) => {
+        let valA = a[problemsSortField];
+        let valB = b[problemsSortField];
+        if (problemsSortAsc) {
+          return valA.localeCompare(valB);
+        } else {
+          return valB.localeCompare(valA);
+        }
+      });
+
+      const tbody = document.getElementById('problemsTableBody');
+      tbody.innerHTML = '';
+
+      if (filtered.length === 0) {
+        tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; color: var(--text-secondary)">No problems found.</td></tr>';
+        return;
+      }
+
+      filtered.forEach(p => {
+        const tr = document.createElement('tr');
+        
+        const sevTd = document.createElement('td');
+        const badge = document.createElement('span');
+        badge.className = `badge ${p.severity === 'ERROR' ? 'badge-failure' : 'badge-warning'}`;
+        badge.innerText = p.severity;
+        sevTd.appendChild(badge);
+
+        const srcTd = document.createElement('td');
+        srcTd.innerText = p.source;
+
+        const keyTd = document.createElement('td');
+        if (p.documentationUrl) {
+          const a = document.createElement('a');
+          a.href = p.documentationUrl;
+          a.target = '_blank';
+          a.style.color = 'var(--accent-color)';
+          a.style.textDecoration = 'none';
+          a.innerText = p.key;
+          keyTd.appendChild(a);
+        } else {
+          keyTd.innerText = p.key;
+        }
+
+        const msgTd = document.createElement('td');
+        msgTd.innerText = p.message;
+
+        const sugTd = document.createElement('td');
+        sugTd.innerText = p.suggestion;
+
+        tr.appendChild(sevTd);
+        tr.appendChild(srcTd);
+        tr.appendChild(keyTd);
+        tr.appendChild(msgTd);
+        tr.appendChild(sugTd);
+        tbody.appendChild(tr);
+      });
+    }
+
+    function sortProblems(field) {
+      if (problemsSortField === field) {
+        problemsSortAsc = !problemsSortAsc;
+      } else {
+        problemsSortField = field;
+        problemsSortAsc = true;
+      }
+      filterProblems();
+    }
+
+    function renderFailures() {
+      const container = document.getElementById('failuresList');
+      container.innerHTML = '';
+
+      if (reportData.failures.length === 0) {
+        container.innerHTML = '<div style="color: var(--text-secondary); text-align: center; margin-top: 40px;">No critical failures recorded.</div>';
+        return;
+      }
+
+      reportData.failures.forEach(f => {
+        const div = document.createElement('div');
+        div.className = 'failure-item';
+
+        const header = document.createElement('div');
+        header.className = 'failure-header';
+        header.innerHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
+                            <span>${f.moduleId}${f.mojo ? ' @ ' + f.mojo : ''}</span>`;
+
+        const stack = document.createElement('pre');
+        stack.className = 'failure-stack';
+        stack.innerHTML = highlightStackTrace(f.message);
+
+        div.appendChild(header);
+        div.appendChild(stack);
+        container.appendChild(div);
+      });
+    }
+
+    function highlightStackTrace(stack) {
+      // Basic styling for stack trace
+      return stack.replace(/at\s+([a-zA-Z0-9_$.]+\.[a-zA-Z0-9_$]+)\(([^)]+)\)/g, (match, method, location) => {
+        return `at <span style="color: #60a5fa">${method}</span>(<span style="color: #34d399">${location}</span>)`;
+      });
+    }
+
+    // High Performance Virtual Scrolling Log Viewer
+    let logLines = [];
+    const LINE_HEIGHT = 22;
+    const VISIBLE_BUFFER = 15;
+    
+    function setupVirtualScroll() {
+      if (!reportData) return;
+      logLines = reportData.logs;
+      filterLogs();
+      
+      const windowEl = document.getElementById('logLinesWindow');
+      windowEl.removeEventListener('scroll', handleLogScroll);
+      windowEl.addEventListener('scroll', handleLogScroll);
+    }
+
+    let filteredLogs = [];
+    function filterLogs() {
+      const search = document.getElementById('logSearch').value.toLowerCase();
+      const mod = document.getElementById('logModuleFilter').value;
+      const level = document.getElementById('logLevelFilter').value;
+
+      filteredLogs = logLines.filter(l => {
+        const matchesSearch = l.message.toLowerCase().includes(search);
+        const matchesModule = mod === 'ALL' || l.moduleId === mod;
+        const matchesLevel = level === 'ALL' || l.level === level;
+        return matchesSearch && matchesModule && matchesLevel;
+      });
+
+      const scroller = document.getElementById('logLinesScrollHeight');
+      scroller.style.height = `${filteredLogs.length * LINE_HEIGHT}px`;
+      
+      const windowEl = document.getElementById('logLinesWindow');
+      windowEl.scrollTop = 0;
+      handleLogScroll();
+    }
+
+    function handleLogScroll() {
+      const windowEl = document.getElementById('logLinesWindow');
+      const scroller = document.getElementById('logLinesScrollHeight');
+      const scrollTop = windowEl.scrollTop;
+      const clientHeight = windowEl.clientHeight;
+
+      const startIndex = Math.max(0, Math.floor(scrollTop / LINE_HEIGHT) - VISIBLE_BUFFER);
+      const endIndex = Math.min(filteredLogs.length, Math.ceil((scrollTop + clientHeight) / LINE_HEIGHT) + VISIBLE_BUFFER);
+
+      // Remove existing lines and add visible ones
+      const oldLines = scroller.querySelectorAll('.log-line');
+      oldLines.forEach(l => l.remove());
+
+      const search = document.getElementById('logSearch').value;
+
+      for (let i = startIndex; i < endIndex; i++) {
+        const entry = filteredLogs[i];
+        const div = document.createElement('div');
+        div.className = `log-line level-${entry.level}`;
+        div.style.position = 'absolute';
+        div.style.top = `${i * LINE_HEIGHT}px`;
+        div.style.height = `${LINE_HEIGHT}px`;
+        div.style.left = '0';
+        div.style.right = '0';
+        div.style.overflow = 'hidden';
+
+        let msg = entry.message;
+        if (search) {
+          const regex = new RegExp(`(${escapeRegExp(search)})`, 'gi');
+          msg = msg.replace(regex, '<span class="highlight-search">$1</span>');
+        }
+
+        div.innerHTML = msg;
+        scroller.appendChild(div);
+      }
+    }
+
+    function escapeRegExp(string) {
+      return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+    }
+
+    function formatDuration(ms) {
+      const s = ms / 1000;
+      const m = s / 60;
+      if (m >= 1) {
+        return `${Math.floor(m)}m ${Math.floor(s % 60)}s`;
+      }
+      return `${s.toFixed(2)}s`;
+    }
+
+    function switchTab(tabId) {
+      currentTab = tabId;
+      document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
+      document.querySelectorAll('.panel').forEach(panel => panel.classList.remove('active'));
+
+      // Find btn based on text
+      const btn = Array.from(document.querySelectorAll('.tab-btn')).find(b => b.innerText.toLowerCase().startsWith(tabId));
+      if (btn) btn.classList.add('active');
+
+      document.getElementById(`${tabId}Panel`).classList.add('active');
+
+      if (tabId === 'logs') {
+        handleLogScroll();
+      }
+    }
+
+    // Init
+    loadReportsList();
+  </script>
+</body>
+</html>
diff --git a/impl/maven-core/src/main/java/org/apache/maven/logging/BuildEventListener.java b/impl/maven-core/src/main/java/org/apache/maven/logging/BuildEventListener.java
index c1d771b5c1..2dee9d3a6e 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/logging/BuildEventListener.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/logging/BuildEventListener.java
@@ -33,12 +33,15 @@ public interface BuildEventListener {
 
     void projectLogMessage(String projectId, LogEvent event);
 
-    void projectFinished(String projectId);
+    void projectFinished(String projectId, String status);
 
     void executionFailure(String projectId, boolean halted, String exception);
 
     void mojoStarted(ExecutionEvent event);
 
+    void mojoFinished(ExecutionEvent event, String status);
+
+
     void finish(int exitCode) throws Exception;
 
     void fail(Throwable t) throws Exception;
diff --git a/impl/maven-core/src/main/java/org/apache/maven/logging/BuildReportEventListener.java b/impl/maven-core/src/main/java/org/apache/maven/logging/BuildReportEventListener.java
new file mode 100644
index 0000000000..4e77bda2d7
--- /dev/null
+++ b/impl/maven-core/src/main/java/org/apache/maven/logging/BuildReportEventListener.java
@@ -0,0 +1,452 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.logging;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.apache.maven.execution.ExecutionEvent;
+import org.eclipse.aether.transfer.TransferEvent;
+
+public class BuildReportEventListener implements BuildEventListener {
+
+    private final BuildEventListener delegate;
+    private Path topDirectory = Paths.get("");
+    private long startTime = System.currentTimeMillis();
+    private long endTime;
+    private List<String> goals = Collections.emptyList();
+    private int degreeOfConcurrency = 1;
+    private String status = "SUCCESS";
+
+    private final Map<String, ModuleInfo> modules = new ConcurrentHashMap<>();
+    private final List<ModuleInfo> orderedModules = new CopyOnWriteArrayList<>();
+    private final Map<Long, MojoInfo> activeMojos = new ConcurrentHashMap<>();
+    private final List<ProblemInfo> problems = new CopyOnWriteArrayList<>();
+    private final List<FailureInfo> failures = new CopyOnWriteArrayList<>();
+    private final List<LogEntry> logs = new CopyOnWriteArrayList<>();
+
+    public BuildReportEventListener(BuildEventListener delegate) {
+        this.delegate = delegate;
+    }
+
+    @Override
+    public void sessionStarted(ExecutionEvent event) {
+        delegate.sessionStarted(event);
+        if (event.getSession() != null) {
+            if (event.getSession().getTopDirectory() != null) {
+                topDirectory = event.getSession().getTopDirectory();
+            }
+            if (event.getSession().getRequest() != null) {
+                goals = event.getSession().getRequest().getGoals();
+                degreeOfConcurrency = event.getSession().getRequest().getDegreeOfConcurrency();
+            }
+            if (event.getSession().getStartTime() != null) {
+                startTime = event.getSession().getStartTime().getTime();
+            }
+        }
+    }
+
+    @Override
+    public void projectStarted(String projectId) {
+        delegate.projectStarted(projectId);
+        ModuleInfo info = new ModuleInfo(projectId);
+        info.startTime = System.currentTimeMillis();
+        modules.put(projectId, info);
+        orderedModules.add(info);
+    }
+
+    @Override
+    public void projectFinished(String projectId, String status) {
+        delegate.projectFinished(projectId, status);
+        ModuleInfo info = modules.get(projectId);
+        if (info != null) {
+            info.endTime = System.currentTimeMillis();
+            info.status = status;
+        }
+    }
+
+    @Override
+    public void executionFailure(String projectId, boolean halted, String exception) {
+        delegate.executionFailure(projectId, halted, exception);
+        status = "FAILURE";
+        MojoInfo activeMojo = activeMojos.get(Thread.currentThread().getId());
+        String mojoId = activeMojo != null ? activeMojo.getId() : null;
+        failures.add(new FailureInfo(projectId, mojoId, exception != null ? exception : "Unknown Cause"));
+    }
+
+    @Override
+    public void mojoStarted(ExecutionEvent event) {
+        delegate.mojoStarted(event);
+        if (event.getMojoExecution() != null) {
+            MojoInfo info = new MojoInfo(
+                event.getMojoExecution().getGroupId(),
+                event.getMojoExecution().getArtifactId(),
+                event.getMojoExecution().getVersion(),
+                event.getMojoExecution().getGoal(),
+                event.getMojoExecution().getExecutionId(),
+                event.getMojoExecution().getLifecyclePhase()
+            );
+            info.startTime = System.currentTimeMillis();
+            activeMojos.put(Thread.currentThread().getId(), info);
+
+            // Add MojoInfo to the corresponding ModuleInfo
+            if (event.getProject() != null) {
+                ModuleInfo moduleInfo = modules.get(event.getProject().getArtifactId());
+                if (moduleInfo != null) {
+                    moduleInfo.mojos.add(info);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void mojoFinished(ExecutionEvent event, String status) {
+        long threadId = Thread.currentThread().getId();
+        MojoInfo info = activeMojos.remove(threadId);
+        if (info != null) {
+            info.endTime = System.currentTimeMillis();
+            info.status = status;
+        }
+    }
+
+    @Override
+    public void projectLogMessage(String projectId, String event) {
+        delegate.projectLogMessage(projectId, event);
+        if (event == null) return;
+
+        long timestamp = System.currentTimeMillis();
+        String cleanMessage = event.replaceAll("\\u001B\\[[;\\d]*[ -/]*[@-~]", "");
+
+        // Determine log level
+        String level = "INFO";
+        if (cleanMessage.contains("[TRACE]")) {
+            level = "TRACE";
+        } else if (cleanMessage.contains("[DEBUG]")) {
+            level = "DEBUG";
+        } else if (cleanMessage.contains("[WARNING]") || cleanMessage.contains("[WARN]")) {
+            level = "WARN";
+        } else if (cleanMessage.contains("[ERROR]")) {
+            level = "ERROR";
+        }
+
+        // Get currently active mojo on this thread
+        MojoInfo activeMojo = activeMojos.get(Thread.currentThread().getId());
+        String mojoId = activeMojo != null ? activeMojo.getId() : null;
+
+        LogEntry logEntry = new LogEntry(timestamp, level, projectId, mojoId, cleanMessage);
+        logs.add(logEntry);
+
+        // Record warnings and errors as problems
+        if ("WARN".equals(level) || "ERROR".equals(level)) {
+            addProblemFromLog(level, projectId, mojoId, cleanMessage);
+        }
+    }
+
+    private void addProblemFromLog(String level, String projectId, String mojoId, String message) {
+        String docUrl = "";
+        int httpIdx = message.indexOf("http://");
+        if (httpIdx == -1) {
+            httpIdx = message.indexOf("https://");
+        }
+        if (httpIdx != -1) {
+            int endIdx = httpIdx;
+            while (endIdx < message.length() && !Character.isWhitespace(message.charAt(endIdx)) 
+                   && message.charAt(endIdx) != ')' && message.charAt(endIdx) != ']') {
+                endIdx++;
+            }
+            docUrl = message.substring(httpIdx, endIdx);
+        }
+
+        String key = "GENERAL";
+        int mngIdx = message.indexOf("MNG-");
+        if (mngIdx != -1) {
+            int endIdx = mngIdx;
+            while (endIdx < message.length() && (Character.isLetterOrDigit(message.charAt(endIdx)) || message.charAt(endIdx) == '-')) {
+                endIdx++;
+            }
+            key = message.substring(mngIdx, endIdx);
+        } else if (mojoId != null) {
+            key = mojoId;
+        }
+
+        String source = projectId != null ? projectId : "core";
+        String suggestion = "Review log details for resolution.";
+        if (message.toLowerCase().contains("use ") || message.toLowerCase().contains("should be")) {
+            suggestion = "Follow the recommended configuration change in the message.";
+        }
+
+        problems.add(new ProblemInfo(level, message, key, source, suggestion, docUrl));
+    }
+
+    @Override
+    public void finish(int exitCode) throws Exception {
+        endTime = System.currentTimeMillis();
+        if (exitCode != 0) {
+            status = "FAILURE";
+        }
+        writeReport();
+        delegate.finish(exitCode);
+    }
+
+    @Override
+    public void fail(Throwable t) throws Exception {
+        endTime = System.currentTimeMillis();
+        status = "FAILURE";
+        failures.add(new FailureInfo("session", null, t != null ? t.toString() : "Build Exception"));
+        writeReport();
+        delegate.fail(t);
+    }
+
+    @Override
+    public void log(String msg) {
+        delegate.log(msg);
+    }
+
+    @Override
+    public void transfer(String projectId, TransferEvent e) {
+        delegate.transfer(projectId, e);
+    }
+
+    private void writeReport() {
+        try {
+            Path reportsDir = topDirectory.resolve(".mvn").resolve("reports");
+            Files.createDirectories(reportsDir);
+
+            String timestamp = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date());
+            Path reportFile = reportsDir.resolve("build-report-" + timestamp + ".json");
+
+            try (BufferedWriter writer = Files.newBufferedWriter(reportFile)) {
+                writer.write("{\n");
+                writer.write("  \"metadata\": {\n");
+                writer.write("    \"status\": \"" + status + "\",\n");
+                writer.write("    \"mavenVersion\": \"" + escapeJson(System.getProperty("maven.version", "4.1.0-SNAPSHOT")) + "\",\n");
+                writer.write("    \"javaVersion\": \"" + escapeJson(System.getProperty("java.version", "unknown")) + "\",\n");
+                writer.write("    \"startTime\": " + startTime + ",\n");
+                writer.write("    \"endTime\": " + endTime + ",\n");
+                writer.write("    \"durationMs\": " + (endTime - startTime) + ",\n");
+                writer.write("    \"goals\": [" + formatStringList(goals) + "],\n");
+                writer.write("    \"threadCount\": " + degreeOfConcurrency + "\n");
+                writer.write("  },\n");
+
+                writer.write("  \"modules\": [\n");
+                for (int i = 0; i < orderedModules.size(); i++) {
+                    ModuleInfo mod = orderedModules.get(i);
+                    writer.write("    {\n");
+                    writer.write("      \"id\": \"" + escapeJson(mod.id) + "\",\n");
+                    writer.write("      \"status\": \"" + mod.status + "\",\n");
+                    writer.write("      \"startTime\": " + mod.startTime + ",\n");
+                    writer.write("      \"endTime\": " + mod.endTime + ",\n");
+                    writer.write("      \"durationMs\": " + (mod.endTime - mod.startTime) + ",\n");
+                    writer.write("      \"mojos\": [\n");
+                    for (int j = 0; j < mod.mojos.size(); j++) {
+                        MojoInfo mojo = mod.mojos.get(j);
+                        writer.write("        {\n");
+                        writer.write("          \"pluginGroupId\": \"" + escapeJson(mojo.groupId) + "\",\n");
+                        writer.write("          \"pluginArtifactId\": \"" + escapeJson(mojo.artifactId) + "\",\n");
+                        writer.write("          \"pluginVersion\": \"" + escapeJson(mojo.version) + "\",\n");
+                        writer.write("          \"goal\": \"" + escapeJson(mojo.goal) + "\",\n");
+                        writer.write("          \"executionId\": \"" + escapeJson(mojo.executionId) + "\",\n");
+                        writer.write("          \"phase\": \"" + escapeJson(mojo.phase) + "\",\n");
+                        writer.write("          \"status\": \"" + mojo.status + "\",\n");
+                        writer.write("          \"startTime\": " + mojo.startTime + ",\n");
+                        writer.write("          \"endTime\": " + mojo.endTime + ",\n");
+                        writer.write("          \"durationMs\": " + (mojo.endTime - mojo.startTime) + "\n");
+                        writer.write("        }" + (j < mod.mojos.size() - 1 ? "," : "") + "\n");
+                    }
+                    writer.write("      ]\n");
+                    writer.write("    }" + (i < orderedModules.size() - 1 ? "," : "") + "\n");
+                }
+                writer.write("  ],\n");
+
+                writer.write("  \"problems\": [\n");
+                for (int i = 0; i < problems.size(); i++) {
+                    ProblemInfo prob = problems.get(i);
+                    writer.write("    {\n");
+                    writer.write("      \"severity\": \"" + prob.severity + "\",\n");
+                    writer.write("      \"message\": \"" + escapeJson(prob.message) + "\",\n");
+                    writer.write("      \"key\": \"" + escapeJson(prob.key) + "\",\n");
+                    writer.write("      \"source\": \"" + escapeJson(prob.source) + "\",\n");
+                    writer.write("      \"suggestion\": \"" + escapeJson(prob.suggestion) + "\",\n");
+                    writer.write("      \"documentationUrl\": \"" + escapeJson(prob.documentationUrl) + "\"\n");
+                    writer.write("    }" + (i < problems.size() - 1 ? "," : "") + "\n");
+                }
+                writer.write("  ],\n");
+
+                writer.write("  \"failures\": [\n");
+                for (int i = 0; i < failures.size(); i++) {
+                    FailureInfo fail = failures.get(i);
+                    writer.write("    {\n");
+                    writer.write("      \"moduleId\": \"" + escapeJson(fail.moduleId) + "\",\n");
+                    writer.write("      \"mojo\": " + (fail.mojoId != null ? "\"" + escapeJson(fail.mojoId) + "\"" : "null") + ",\n");
+                    writer.write("      \"message\": \"" + escapeJson(fail.message) + "\"\n");
+                    writer.write("    }" + (i < failures.size() - 1 ? "," : "") + "\n");
+                }
+                writer.write("  ],\n");
+
+                writer.write("  \"logs\": [\n");
+                for (int i = 0; i < logs.size(); i++) {
+                    LogEntry entry = logs.get(i);
+                    writer.write("    {\n");
+                    writer.write("      \"timestamp\": " + entry.timestamp + ",\n");
+                    writer.write("      \"level\": \"" + entry.level + "\",\n");
+                    writer.write("      \"moduleId\": " + (entry.moduleId != null ? "\"" + escapeJson(entry.moduleId) + "\"" : "null") + ",\n");
+                    writer.write("      \"mojoId\": " + (entry.mojoId != null ? "\"" + escapeJson(entry.mojoId) + "\"" : "null") + ",\n");
+                    writer.write("      \"message\": \"" + escapeJson(entry.message) + "\"\n");
+                    writer.write("    }" + (i < logs.size() - 1 ? "," : "") + "\n");
+                }
+                writer.write("  ]\n");
+                writer.write("}\n");
+            }
+        } catch (IOException e) {
+            // Silently ignore report writing failures to not crash maven build
+        }
+    }
+
+    private static String formatStringList(List<String> list) {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < list.size(); i++) {
+            sb.append("\"").append(escapeJson(list.get(i))).append("\"");
+            if (i < list.size() - 1) {
+                sb.append(", ");
+            }
+        }
+        return sb.toString();
+    }
+
+    private static String escapeJson(String string) {
+        if (string == null) {
+            return "";
+        }
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < string.length(); i++) {
+            char ch = string.charAt(i);
+            switch (ch) {
+                case '"': sb.append("\\\""); break;
+                case '\\': sb.append("\\\\"); break;
+                case '\b': sb.append("\\b"); break;
+                case '\f': sb.append("\\f"); break;
+                case '\n': sb.append("\\n"); break;
+                case '\r': sb.append("\\r"); break;
+                case '\t': sb.append("\\t"); break;
+                default:
+                    if (ch < ' ') {
+                        String t = "000" + Integer.toHexString(ch);
+                        sb.append("\\u" + t.substring(t.length() - 4));
+                    } else {
+                        sb.append(ch);
+                    }
+            }
+        }
+        return sb.toString();
+    }
+
+    private static class ModuleInfo {
+        final String id;
+        String status = "SKIPPED";
+        long startTime;
+        long endTime;
+        final List<MojoInfo> mojos = new CopyOnWriteArrayList<>();
+
+        ModuleInfo(String id) {
+            this.id = id;
+        }
+    }
+
+    private static class MojoInfo {
+        final String groupId;
+        final String artifactId;
+        final String version;
+        final String goal;
+        final String executionId;
+        final String phase;
+        String status = "SKIPPED";
+        long startTime;
+        long endTime;
+
+        MojoInfo(String groupId, String artifactId, String version, String goal, String executionId, String phase) {
+            this.groupId = groupId;
+            this.artifactId = artifactId;
+            this.version = version;
+            this.goal = goal;
+            this.executionId = executionId;
+            this.phase = phase;
+        }
+
+        String getId() {
+            return groupId + ":" + artifactId + ":" + version + ":" + goal + ":" + executionId;
+        }
+    }
+
+    private static class ProblemInfo {
+        final String severity;
+        final String message;
+        final String key;
+        final String source;
+        final String suggestion;
+        final String documentationUrl;
+
+        ProblemInfo(String severity, String message, String key, String source, String suggestion, String documentationUrl) {
+            this.severity = severity;
+            this.message = message;
+            this.key = key;
+            this.source = source;
+            this.suggestion = suggestion;
+            this.documentationUrl = documentationUrl != null ? documentationUrl : "";
+        }
+    }
+
+    private static class FailureInfo {
+        final String moduleId;
+        final String mojoId;
+        final String message;
+
+        FailureInfo(String moduleId, String mojoId, String message) {
+            this.moduleId = moduleId;
+            this.mojoId = mojoId;
+            this.message = message;
+        }
+    }
+
+    private static class LogEntry {
+        final long timestamp;
+        final String level;
+        final String moduleId;
+        final String mojoId;
+        final String message;
+
+        LogEntry(long timestamp, String level, String moduleId, String mojoId, String message) {
+            this.timestamp = timestamp;
+            this.level = level;
+            this.moduleId = moduleId;
+            this.mojoId = mojoId;
+            this.message = message;
+        }
+    }
+}
diff --git a/impl/maven-core/src/main/java/org/apache/maven/logging/SimpleBuildEventListener.java b/impl/maven-core/src/main/java/org/apache/maven/logging/SimpleBuildEventListener.java
index 37c49a92c1..83ab4a0e19 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/logging/SimpleBuildEventListener.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/logging/SimpleBuildEventListener.java
@@ -45,7 +45,11 @@ public void projectLogMessage(String projectId, LogEvent event) {
     }
 
     @Override
-    public void projectFinished(String projectId) {}
+    public void projectFinished(String projectId, String status) {}
+
+    @Override
+    public void mojoFinished(ExecutionEvent event, String status) {}
+
 
     @Override
     public void executionFailure(String projectId, boolean halted, String exception) {}