From 88765b4f50443c73a93f1a7ae4a6f88ea886d1c4 Mon Sep 17 00:00:00 2001 From: Mondo Diaz Date: Mon, 2 Feb 2026 16:29:37 -0600 Subject: [PATCH] Show missing dependencies in dependency graph instead of failing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When dependencies are not cached on the server (common since we removed proactive caching), the dependency graph now: - Continues resolving what it can find - Shows missing dependencies in a separate section with amber styling - Displays the constraint and which package required them - Updates the header stats to show "X cached • Y not cached" This provides a better user experience than showing an error when some dependencies haven't been downloaded yet. --- backend/app/dependencies.py | 17 ++++-- backend/app/schemas.py | 9 +++ frontend/src/components/DependencyGraph.css | 61 +++++++++++++++++++++ frontend/src/components/DependencyGraph.tsx | 22 +++++++- frontend/src/types.ts | 8 +++ 5 files changed, 111 insertions(+), 6 deletions(-) diff --git a/backend/app/dependencies.py b/backend/app/dependencies.py index c696b97..7f2e172 100644 --- a/backend/app/dependencies.py +++ b/backend/app/dependencies.py @@ -42,6 +42,7 @@ from .schemas import ( ResolvedArtifact, DependencyResolutionResponse, DependencyConflict, + MissingDependency, PaginationMeta, ) @@ -690,6 +691,8 @@ def resolve_dependencies( # Track resolved artifacts and their versions resolved_artifacts: Dict[str, ResolvedArtifact] = {} + # Track missing dependencies (not cached on server) + missing_dependencies: List[MissingDependency] = [] # Track version requirements for conflict detection version_requirements: Dict[str, List[Dict[str, Any]]] = {} # pkg_key -> [(version, required_by)] # Track visiting/visited for cycle detection @@ -773,12 +776,15 @@ def resolve_dependencies( ) if not resolved_dep: + # Dependency not cached on server - track as missing but continue constraint = dep.version_constraint or dep.tag_constraint - raise DependencyNotFoundError( - dep.dependency_project, - dep.dependency_package, - constraint, - ) + missing_dependencies.append(MissingDependency( + project=dep.dependency_project, + package=dep.dependency_package, + constraint=constraint, + required_by=pkg_key, + )) + continue dep_artifact_id, dep_version, dep_size = resolved_dep _resolve_recursive( @@ -828,6 +834,7 @@ def resolve_dependencies( "ref": ref, }, resolved=resolved_list, + missing=missing_dependencies, total_size=total_size, artifact_count=len(resolved_list), ) diff --git a/backend/app/schemas.py b/backend/app/schemas.py index d9683b7..481891a 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -1033,10 +1033,19 @@ class ResolvedArtifact(BaseModel): download_url: str +class MissingDependency(BaseModel): + """A dependency that could not be resolved (not cached on server)""" + project: str + package: str + constraint: Optional[str] = None + required_by: Optional[str] = None + + class DependencyResolutionResponse(BaseModel): """Response from dependency resolution endpoint""" requested: Dict[str, str] # project, package, ref resolved: List[ResolvedArtifact] + missing: List[MissingDependency] = [] total_size: int artifact_count: int diff --git a/frontend/src/components/DependencyGraph.css b/frontend/src/components/DependencyGraph.css index 9374b63..9665934 100644 --- a/frontend/src/components/DependencyGraph.css +++ b/frontend/src/components/DependencyGraph.css @@ -55,6 +55,10 @@ font-size: 0.8125rem; } +.missing-count { + color: #f59e0b; +} + .close-btn { background: transparent; border: none; @@ -314,6 +318,63 @@ font-size: 0.75rem; } +/* Missing Dependencies */ +.missing-dependencies { + border-top: 1px solid var(--border-primary); + padding: 16px 20px; + background: rgba(245, 158, 11, 0.05); + max-height: 200px; + overflow-y: auto; +} + +.missing-dependencies h3 { + margin: 0 0 8px 0; + font-size: 0.875rem; + font-weight: 600; + color: #f59e0b; +} + +.missing-hint { + margin: 0 0 12px 0; + font-size: 0.75rem; + color: var(--text-muted); +} + +.missing-list { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.missing-item { + display: inline-flex; + align-items: center; + gap: 4px; + background: var(--bg-tertiary); + border: 1px solid rgba(245, 158, 11, 0.3); + border-radius: var(--radius-sm); + padding: 4px 8px; + font-size: 0.75rem; +} + +.missing-name { + font-family: 'JetBrains Mono', monospace; + color: var(--text-secondary); +} + +.missing-constraint { + color: var(--text-muted); + font-family: 'JetBrains Mono', monospace; +} + +.missing-required-by { + color: var(--text-muted); + font-size: 0.6875rem; +} + /* Responsive */ @media (max-width: 768px) { .dependency-graph-modal { diff --git a/frontend/src/components/DependencyGraph.tsx b/frontend/src/components/DependencyGraph.tsx index 475591b..89e52dd 100644 --- a/frontend/src/components/DependencyGraph.tsx +++ b/frontend/src/components/DependencyGraph.tsx @@ -244,7 +244,11 @@ function DependencyGraph({ projectName, packageName, tagName, onClose }: Depende {projectName}/{packageName} @ {tagName} {resolution && ( - {resolution.artifact_count} packages • {formatBytes(resolution.total_size)} total + {resolution.artifact_count} cached + {resolution.missing && resolution.missing.length > 0 && ( + • {resolution.missing.length} not cached + )} + • {formatBytes(resolution.total_size)} total )} @@ -307,6 +311,22 @@ function DependencyGraph({ projectName, packageName, tagName, onClose }: Depende )} + {resolution && resolution.missing && resolution.missing.length > 0 && ( +
+

Not Cached ({resolution.missing.length})

+

These dependencies are referenced but not yet cached on the server.

+ +
+ )} + {hoveredNode && (
{hoveredNode.project}/{hoveredNode.package} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 8469560..f4f6412 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -433,6 +433,13 @@ export interface ResolvedArtifact { download_url: string; } +export interface MissingDependency { + project: string; + package: string; + constraint: string | null; + required_by: string | null; +} + export interface DependencyResolutionResponse { requested: { project: string; @@ -440,6 +447,7 @@ export interface DependencyResolutionResponse { ref: string; }; resolved: ResolvedArtifact[]; + missing: MissingDependency[]; total_size: number; artifact_count: number; }