diff --git a/apps/codecov-api/core/admin.py b/apps/codecov-api/core/admin.py
index 1f70a78f61..914af39a6f 100644
--- a/apps/codecov-api/core/admin.py
+++ b/apps/codecov-api/core/admin.py
@@ -18,6 +18,7 @@
from shared.django_apps.reports.models import ReportType
from shared.django_apps.ta_timeseries.models import Testrun
from shared.django_apps.timeseries.models import Measurement, MeasurementName
+from shared.django_apps.upload_breadcrumbs.models import UploadBreadcrumb
from shared.django_apps.utils.paginator import EstimatedCountPaginator
from shared.helpers.redis import get_redis_connection
from shared.reports.enums import UploadState
@@ -270,6 +271,7 @@ class CommitAdmin(AdminMixin, admin.ModelAdmin):
"deleted",
"notified",
"reprocess_actions",
+ "upload_pipeline_timeline",
)
fields = readonly_fields
paginator = EstimatedCountPaginator
@@ -359,6 +361,84 @@ def reprocess_actions(self, obj):
return format_html("
{}
", format_html("".join(buttons)))
+ @admin.display(description="Upload Pipeline Timeline")
+ def upload_pipeline_timeline(self, obj):
+ if obj.pk is None:
+ return ""
+
+ breadcrumbs = UploadBreadcrumb.objects.filter(
+ commit_sha=obj.commitid,
+ repo_id=obj.repository.repoid,
+ ).order_by("created_at")[:200]
+
+ if not breadcrumbs:
+ return format_html("No breadcrumbs recorded for this commit.")
+
+ rows = []
+ for bc in breadcrumbs:
+ data = bc.breadcrumb_data or {}
+ milestone = data.get("milestone", "")
+ error = data.get("error", "")
+ error_text = data.get("error_text", "")
+ task_name = data.get("task_name", "")
+ parent_task_id = data.get("parent_task_id", "")
+ endpoint = data.get("endpoint", "")
+
+ if error:
+ color = "#d32f2f"
+ elif milestone in ("lac", "lr", "uc", "ns"):
+ color = "#388e3c"
+ elif milestone in ("la", "pu", "nt"):
+ color = "#1565c0"
+ else:
+ color = "#555"
+
+ upload_ids_str = (
+ ", ".join(str(uid) for uid in bc.upload_ids) if bc.upload_ids else ""
+ )
+
+ detail_parts = []
+ if milestone:
+ detail_parts.append(f"{milestone}")
+ if endpoint:
+ detail_parts.append(f"endpoint={endpoint}")
+ if error:
+ detail_parts.append(f'err={error}')
+ if error_text:
+ detail_parts.append(
+ f'{error_text[:120]}'
+ )
+ detail = " · ".join(detail_parts) if detail_parts else "—"
+
+ rows.append(
+ f""
+ f'| '
+ f"{bc.created_at:%Y-%m-%d %H:%M:%S} | "
+ f''
+ f"{task_name} | "
+ f''
+ f"{parent_task_id} | "
+ f'{detail} | '
+ f''
+ f"{upload_ids_str} | "
+ f"
"
+ )
+
+ table = (
+ ''
+ ""
+ '| Time | '
+ 'Task | '
+ 'Parent Task ID | '
+ 'Detail | '
+ 'Upload IDs | '
+ "
" + "".join(rows) + "
"
+ )
+
+ return format_html(
+ '{}
', format_html(table)
+ )
+
def _reprocess_uploads(
self, request, commit: Commit, config: ReprocessConfig
) -> HttpResponseRedirect: