Skip to content

Epic/package#18

Open
LofoWalker wants to merge 2 commits intomainfrom
epic/package
Open

Epic/package#18
LofoWalker wants to merge 2 commits intomainfrom
epic/package

Conversation

@LofoWalker
Copy link
Owner

No description provided.

Copilot AI review requested due to automatic review settings February 20, 2026 22:54
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements the “Packages” epic end-to-end: backend persistence + import/list APIs, and a new dashboard/packages UI flow to import dependencies (lockfile upload or paste) and browse the package list.

Changes:

  • Add backend domain/model + DB migration for packages, with import (lockfile/list) and list endpoints.
  • Add frontend packages page with search + infinite scroll, plus upload/paste import components and client API bindings.
  • Update dashboard “Get Started” steps and dashboard stats plumbing to reflect budget/packages progress.

Reviewed changes

Copilot reviewed 32 out of 39 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
apps/web/src/pages/PackagesPage.tsx New packages page (search, infinite scroll, import actions).
apps/web/src/pages/CompanyDashboardPage.tsx Updates “Get Started” steps UI/state logic and links to packages.
apps/web/src/features/packages/index.ts Barrel exports for packages feature components/API/types.
apps/web/src/features/packages/api.ts Frontend API client for packages list/import endpoints.
apps/web/src/features/packages/PastePackagesDialog.tsx UI dialog to import packages via pasted list.
apps/web/src/features/packages/PackageCard.tsx UI component for rendering a package row/card.
apps/web/src/features/packages/FileDropzone.tsx UI component for lockfile drag/drop + file picker.
apps/web/src/features/budget/BudgetSetupForm.tsx Refreshes dashboard state after budget is set.
apps/web/src/App.tsx Adds /dashboard/packages route.
apps/audit.md Removes audit document from the repo.
apps/api/src/main/resources/db/migration/V9__create_packages_table.sql Creates packages table + indexes.
apps/api/src/main/java/com/upkeep/infrastructure/adapter/out/persistence/pkg/PackageEntity.java JPA entity for packages.
apps/api/src/main/java/com/upkeep/infrastructure/adapter/out/persistence/pkg/PackageMapper.java Maps domain Package ↔ persistence entity.
apps/api/src/main/java/com/upkeep/infrastructure/adapter/out/persistence/pkg/PackageJpaRepository.java Panache-backed repository implementing PackageRepository.
apps/api/src/main/java/com/upkeep/application/port/out/pkg/PackageRepository.java Outbound port for package persistence operations.
apps/api/src/main/java/com/upkeep/application/port/out/pkg/LockfileParser.java Outbound port for lockfile parsing.
apps/api/src/main/java/com/upkeep/infrastructure/adapter/out/parser/LockfileParserAdapter.java Parser adapter for package-lock.json and yarn.lock.
apps/api/src/main/java/com/upkeep/domain/model/pkg/PackageId.java Domain identifier for Package.
apps/api/src/main/java/com/upkeep/domain/model/pkg/Package.java Domain model + npm name validation.
apps/api/src/main/java/com/upkeep/application/port/in/pkg/ImportPackagesFromLockfileUseCase.java Use-case port for lockfile import.
apps/api/src/main/java/com/upkeep/application/port/in/pkg/ImportPackagesFromListUseCase.java Use-case port for list import.
apps/api/src/main/java/com/upkeep/application/port/in/pkg/ListCompanyPackagesUseCase.java Use-case port for listing packages with pagination/search.
apps/api/src/main/java/com/upkeep/application/usecase/ImportPackagesFromLockfileUseCaseImpl.java Implements lockfile import flow.
apps/api/src/main/java/com/upkeep/application/usecase/ImportPackagesFromListUseCaseImpl.java Implements list import flow.
apps/api/src/main/java/com/upkeep/application/usecase/ListCompanyPackagesUseCaseImpl.java Implements package listing + membership check.
apps/api/src/main/java/com/upkeep/infrastructure/adapter/in/rest/pkg/PackageResource.java REST endpoints for list + imports.
apps/api/src/main/java/com/upkeep/infrastructure/adapter/in/rest/pkg/PackageListResponse.java DTO for list response.
apps/api/src/main/java/com/upkeep/infrastructure/adapter/in/rest/pkg/ImportLockfileRequest.java DTO for lockfile import request.
apps/api/src/main/java/com/upkeep/infrastructure/adapter/in/rest/pkg/ImportResultResponse.java DTO for lockfile import response.
apps/api/src/main/java/com/upkeep/infrastructure/adapter/in/rest/pkg/ImportListRequest.java DTO for list import request.
apps/api/src/main/java/com/upkeep/infrastructure/adapter/in/rest/pkg/ImportListResultResponse.java DTO for list import response.
apps/api/src/main/java/com/upkeep/application/usecase/GetCompanyDashboardUseCaseImpl.java Adds hasBudget/hasPackages computation to dashboard stats.
apps/api/src/test/java/com/upkeep/infrastructure/adapter/out/parser/LockfileParserAdapterTest.java Unit tests for lockfile parsing.
apps/api/src/test/java/com/upkeep/domain/model/pkg/PackageTest.java Unit tests for package name validation + creation.
apps/api/src/test/java/com/upkeep/application/usecase/ListCompanyPackagesUseCaseImplTest.java Unit tests for listing use case.
apps/api/src/test/java/com/upkeep/application/usecase/ImportPackagesFromLockfileUseCaseImplTest.java Unit tests for lockfile import use case.
apps/api/src/test/java/com/upkeep/application/usecase/ImportPackagesFromListUseCaseImplTest.java Unit tests for list import use case.
apps/api/src/test/java/com/upkeep/application/usecase/GetCompanyDashboardUseCaseImplTest.java Updates tests for new dashboard dependencies + hasBudget case.
Copilot-Processing.md Tracks implementation plan/checklist for the epic.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +140 to +145
return tokenService.validateAccessToken(accessToken);
}

private Response unauthorizedResponse() {
return Response.status(401)
.entity(ApiResponse.error(ApiError.of("UNAUTHORIZED", "Authentication required", null)))
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validateToken forwards to tokenService.validateAccessToken, which throws IllegalArgumentException for invalid/expired tokens (see JwtTokenService). Since there is no mapper for IllegalArgumentException, an invalid cookie will currently bubble up as a 500 instead of a 401. Catch the token parse exception here (or introduce a dedicated access-token exception/mapper) and return unauthorizedResponse() / INVALID_TOKEN consistently.

Suggested change
return tokenService.validateAccessToken(accessToken);
}
private Response unauthorizedResponse() {
return Response.status(401)
.entity(ApiResponse.error(ApiError.of("UNAUTHORIZED", "Authentication required", null)))
try {
return tokenService.validateAccessToken(accessToken);
} catch (IllegalArgumentException ex) {
// Invalid or expired token; treat as unauthorized
return null;
}
}
private Response unauthorizedResponse() {
return Response.status(401)
.entity(ApiResponse.error(ApiError.of("INVALID_TOKEN", "Authentication required", null)))

Copilot uses AI. Check for mistakes.
Comment on lines +214 to 216
<Button variant="outline" size="sm" disabled={!dashboard.stats.hasBudget || !dashboard.stats.hasPackages}>
Allocate
<ArrowRight className="ml-2 h-4 w-4" />
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The step 3 "Allocate" button can now become enabled when hasBudget and hasPackages are true (because the Get Started card now renders until hasAllocations is true), but the button has no onClick handler—so it becomes a dead-end CTA. Wire it to navigate to the allocations flow (e.g. /dashboard/allocations) or keep it disabled until the route/feature exists.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +57
@GET
public Response listPackages(@CookieParam(ACCESS_TOKEN_COOKIE) String accessToken,
@PathParam("companyId") String companyId,
@QueryParam("search") String search,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("50") int size) {
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

page/size query params are passed through without validation. Values like size=0 or negative numbers can lead to invalid offsets and can break pagination downstream (e.g., Panache page size 0). Add basic bounds checks (page >= 0, 1 <= size <= max) and return a 400 with a clear error when out of range.

Copilot uses AI. Check for mistakes.
Comment on lines +49 to +56
const handleFileChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files && files.length > 0) {
onFileAccepted(files[0]);
}
if (inputRef.current) {
inputRef.current.value = '';
}
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleFileChange calls onFileAccepted for any selected file, but drag/drop path filters via isAcceptedFile. This means browsing can submit unsupported filenames (anything matching accept=.json,.lock) and will likely fail server-side. Apply the same isAcceptedFile check in handleFileChange (and ideally surface a user-facing error when rejected).

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +35
validateName(name);
return new Package(PackageId.generate(), companyId, name, "npm", Instant.now());
}

public static Package reconstitute(PackageId id, CompanyId companyId, String name,
String registry, Instant importedAt) {
return new Package(id, companyId, name, registry, importedAt);
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Package.create validates name using name.trim() (via isValidNpmPackageName) but then persists the original, untrimmed name. This allows values like "lodash " to pass validation while violating the intended invariant and can lead to confusing duplicates/search behavior. Normalize the value (at least trim()) before storing it in the domain object.

Suggested change
validateName(name);
return new Package(PackageId.generate(), companyId, name, "npm", Instant.now());
}
public static Package reconstitute(PackageId id, CompanyId companyId, String name,
String registry, Instant importedAt) {
return new Package(id, companyId, name, registry, importedAt);
String normalizedName = name == null ? null : name.trim();
validateName(normalizedName);
return new Package(PackageId.generate(), companyId, normalizedName, "npm", Instant.now());
}
public static Package reconstitute(PackageId id, CompanyId companyId, String name,
String registry, Instant importedAt) {
String normalizedName = name == null ? null : name.trim();
return new Package(id, companyId, normalizedName, registry, importedAt);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants