rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI
CI / Unit Tests (pull_request) Successful in 5m46s
CI / Lint (pull_request) Failing after 3m49s
CI / E2E Tests (pull_request) Has been skipped
CI / Fresh-Linux Docker Deploy (pull_request) Has been skipped
CI / Assistant Split Regression (pull_request) Failing after 35s
CI / Architecture Guardrails (pull_request) Failing after 2m14s
CI / Typecheck (pull_request) Successful in 4m22s
CI / Build (pull_request) Has been skipped
CI / Release Images (pull_request) Has been skipped

- @capakraken/* → @nexus/* across 12 packages (root + 11 workspaces),
  1551 import lines migrated via codemod
- User-visible brand strings renamed (emails, page titles, PWA
  manifest, mobile header, MFA backup-codes header, tooltips, signin
  page, invite page, weekly digest, install prompt)
- TOTP issuer "CapaKraken" → "Nexus" (existing secrets still valid;
  re-enrollment relabels them in users' authenticator apps)
- Function rename: assertCapaKrakenDbTarget → assertNexusDbTarget
- LocalStorage migration shim in apps/web/src/app/layout.tsx copies
  capakraken_* → nexus_* on first load (guarded by nexus_migrated_v1
  sentinel; runs once per browser, then never again)
- Service-worker cache name capakraken-v2 → nexus-v2 with one-time
  caches.delete('capakraken-v2') from the same shim
- Email-domain fixtures @capakraken.{dev,app} → @nexus.{dev,app} in
  seed data, e2e specs, SMTP default fallback
- Dockerfile.dev / Dockerfile.prod / all .github/workflows/*.yml
  pnpm --filter @capakraken/* → @nexus/*
- README, CLAUDE.md, LEARNINGS.md, all docs/*.md, .env.example,
  tooling/deploy/.env.production.example brand sweep

Phase 1 deliberately leaves untouched (handled in Phase 3 cutover):
- PostgreSQL DB name "capakraken" and POSTGRES_USER "capakraken"
- Volume names capakraken_pgdata etc.
- Compose project name "capakraken" / "capakraken-prod"
- db-target-guard default expectedDatabase
- env-var CAPAKRAKEN_EXPECTED_DB_NAME
- Container DNS names in docker-compose.ci.yml

Quality gates green: pnpm typecheck (7/7), pnpm test:unit (7/7),
pnpm lint (0 errors), check:exports/imports/architecture all pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 15:10:44 +02:00
parent d9a7ec0338
commit 4a5edeef3e
941 changed files with 24475 additions and 16760 deletions
@@ -2,20 +2,16 @@ import {
DISPO_REQUIRED_ROLE_SEEDS,
DISPO_UTILIZATION_CATEGORIES,
normalizeDispoRoleToken,
} from "@capakraken/shared";
import type { Prisma } from "@capakraken/db";
} from "@nexus/shared";
import type { Prisma } from "@nexus/db";
import {
AllocationStatus,
ImportBatchStatus,
ProjectStatus,
StagedRecordStatus,
VacationStatus,
} from "@capakraken/db";
import {
buildBatchSummaryEntry,
buildFallbackAccentureEmail,
toJsonObject,
} from "./shared.js";
} from "@nexus/db";
import { buildBatchSummaryEntry, buildFallbackAccentureEmail, toJsonObject } from "./shared.js";
import { recomputeResourceValueScores } from "../resource/recompute-resource-value-scores.js";
import { classifyDispoProject } from "./tbd-projects.js";
import type { CommitDbClient, MergedStagedResource, TxClient } from "./commit-dispo-batch-types.js";
@@ -27,10 +23,7 @@ import {
mergeStagedResources,
parseWeekdayAvailability,
} from "./build-dispo-maps.js";
import {
aggregateAssignments,
deriveOverlayAvailability,
} from "./determine-placement.js";
import { aggregateAssignments, deriveOverlayAvailability } from "./determine-placement.js";
export interface CommitDispoImportBatchInput {
allowTbdUnresolved?: boolean;
@@ -147,14 +140,20 @@ function buildResourceData(
chapter: resource.chapter,
chargeabilityTarget: resource.chargeabilityTarget ?? defaultChargeabilityTarget,
clientUnitId: resource.clientUnitName
? (maps.clientIdByCode.get(resource.clientUnitName) ?? maps.clientIdByName.get(resource.clientUnitName.toLowerCase()) ?? null)
? (maps.clientIdByCode.get(resource.clientUnitName) ??
maps.clientIdByName.get(resource.clientUnitName.toLowerCase()) ??
null)
: null,
countryId: resource.countryCode
? (maps.countryIdByCode.get(resource.countryCode) ?? null)
: null,
countryId: resource.countryCode ? (maps.countryIdByCode.get(resource.countryCode) ?? null) : null,
displayName: resource.displayName ?? resource.canonicalExternalId,
email: resource.email ?? buildFallbackAccentureEmail(resource.canonicalExternalId),
fte: resource.fte ?? 1,
lcrCents: resource.lcrCents ?? 0,
managementLevelGroupId: resource.managementLevelGroupName ? (managementGroup?.id ?? null) : null,
managementLevelGroupId: resource.managementLevelGroupName
? (managementGroup?.id ?? null)
: null,
managementLevelId: resource.managementLevelName
? (maps.managementLevelIdByName.get(resource.managementLevelName) ?? null)
: null,
@@ -291,7 +290,13 @@ async function commitResources(
}
}
return { resourceIdByKey, resourceRoleNameByKey, updatedEntitlements, updatedResourceAvailabilities, upsertedResourceRoles };
return {
resourceIdByKey,
resourceRoleNameByKey,
updatedEntitlements,
updatedResourceAvailabilities,
upsertedResourceRoles,
};
}
async function commitProjects(
@@ -326,7 +331,9 @@ async function commitProjects(
update: {
allocationType: stagedProject.allocationType ?? classification.allocationType,
budgetCents: 0,
clientId: stagedProject.clientCode ? (maps.clientIdByCode.get(stagedProject.clientCode) ?? null) : null,
clientId: stagedProject.clientCode
? (maps.clientIdByCode.get(stagedProject.clientCode) ?? null)
: null,
dynamicFields,
endDate: normalizeDate(stagedProject.endDate ?? stagedProject.startDate ?? new Date()),
name: stagedProject.name ?? shortCode,
@@ -351,7 +358,9 @@ async function commitProjects(
utilizationCategoryId: stagedProject.utilizationCategoryCode
? (maps.utilizationCategoryIdByCode.get(stagedProject.utilizationCategoryCode) ?? null)
: null,
clientId: stagedProject.clientCode ? (maps.clientIdByCode.get(stagedProject.clientCode) ?? null) : null,
clientId: stagedProject.clientCode
? (maps.clientIdByCode.get(stagedProject.clientCode) ?? null)
: null,
dynamicFields,
},
select: { id: true },
@@ -368,202 +377,266 @@ export async function commitDispoImportBatch(
): Promise<CommitDispoImportBatchResult> {
const validation = await validateDispoBatch(db, input);
const result = await db.$transaction(async (tx) => {
await tx.importBatch.update({
where: { id: validation.batchId },
data: { status: ImportBatchStatus.COMMITTING },
});
await upsertUtilizationCategories(tx);
await upsertRoleSeeds(tx);
const [
stagedResources, stagedProjects, stagedAssignments, stagedVacations,
stagedAvailabilityRules, adminUser, countries, metroCities,
managementLevelGroups, managementLevels, clients, orgUnits, roles,
utilizationCategories,
] = await Promise.all([
tx.stagedResource.findMany({ where: { importBatchId: validation.batchId } }),
tx.stagedProject.findMany({ where: { importBatchId: validation.batchId } }),
tx.stagedAssignment.findMany({ where: { importBatchId: validation.batchId } }),
tx.stagedVacation.findMany({ where: { importBatchId: validation.batchId } }),
tx.stagedAvailabilityRule.findMany({ where: { importBatchId: validation.batchId } }),
tx.user.findFirst({ where: { systemRole: "ADMIN" }, select: { id: true } }),
tx.country.findMany({ select: { id: true, code: true } }),
tx.metroCity.findMany({ select: { id: true, name: true } }),
tx.managementLevelGroup.findMany({ select: { id: true, name: true, targetPercentage: true } }),
tx.managementLevel.findMany({ select: { id: true, name: true } }),
tx.client.findMany({ select: { id: true, code: true, name: true } }),
tx.orgUnit.findMany({ select: { id: true, level: true, name: true } }),
tx.role.findMany({ select: { id: true, name: true } }),
tx.utilizationCategory.findMany({ select: { id: true, code: true } }),
]);
if (!adminUser) {
throw new Error("Cannot commit Dispo import without an ADMIN user in the database");
}
const maps = buildReferenceDataMaps({
clients, countries, managementLevelGroups, managementLevels,
metroCities, orgUnits, roles, utilizationCategories,
});
const mergedResources = mergeStagedResources(stagedResources);
await ensureInferredRolesExist(tx, mergedResources, maps.roleIdByName);
const resourceResult = await commitResources(
tx, mergedResources, maps, validation.batchId,
stagedVacations, stagedAvailabilityRules,
);
const projectIdByShortCode = await commitProjects(
tx, stagedProjects, maps, validation.batchId, input.importTbdProjects ?? false,
);
// Commit assignments
const aggregatedAssignments = aggregateAssignments(
stagedAssignments, resourceResult.resourceIdByKey, projectIdByShortCode,
maps.roleIdByName, resourceResult.resourceRoleNameByKey, input.importTbdProjects ?? false,
);
for (const assignment of aggregatedAssignments) {
const dailyCostCents = Math.round(
assignment.hoursPerDay * (mergedResources.get(assignment.resourceKey)?.lcrCents ?? 0),
);
const metadata = {
dispoImport: {
importBatchId: validation.batchId,
sourceDates: assignment.sourceDates,
utilizationCategoryCode: assignment.utilizationCategoryCode,
},
} as Prisma.InputJsonValue;
await tx.assignment.upsert({
where: {
unique_assignment: {
resourceId: assignment.resourceId,
projectId: assignment.projectId,
startDate: assignment.startDate,
endDate: assignment.endDate,
},
},
update: {
dailyCostCents, metadata,
percentage: assignment.percentage,
role: assignment.roleName,
roleId: assignment.roleId,
status: AllocationStatus.PROPOSED,
},
create: {
dailyCostCents, metadata,
endDate: assignment.endDate,
hoursPerDay: assignment.hoursPerDay,
percentage: assignment.percentage,
projectId: assignment.projectId,
resourceId: assignment.resourceId,
role: assignment.roleName,
roleId: assignment.roleId,
startDate: assignment.startDate,
status: AllocationStatus.PROPOSED,
},
const result = await db.$transaction(
async (tx) => {
await tx.importBatch.update({
where: { id: validation.batchId },
data: { status: ImportBatchStatus.COMMITTING },
});
await tx.resourceRole.upsert({
where: { resourceId_roleId: { resourceId: assignment.resourceId, roleId: assignment.roleId } },
update: {},
create: { resourceId: assignment.resourceId, roleId: assignment.roleId },
});
resourceResult.upsertedResourceRoles += 1;
}
await upsertUtilizationCategories(tx);
await upsertRoleSeeds(tx);
// Commit vacations
for (const stagedVacation of stagedVacations) {
const resourceId = resourceResult.resourceIdByKey.get(stagedVacation.resourceExternalId);
if (!resourceId) {
throw new Error(`Unable to resolve resource "${stagedVacation.resourceExternalId}" during vacation commit`);
const [
stagedResources,
stagedProjects,
stagedAssignments,
stagedVacations,
stagedAvailabilityRules,
adminUser,
countries,
metroCities,
managementLevelGroups,
managementLevels,
clients,
orgUnits,
roles,
utilizationCategories,
] = await Promise.all([
tx.stagedResource.findMany({ where: { importBatchId: validation.batchId } }),
tx.stagedProject.findMany({ where: { importBatchId: validation.batchId } }),
tx.stagedAssignment.findMany({ where: { importBatchId: validation.batchId } }),
tx.stagedVacation.findMany({ where: { importBatchId: validation.batchId } }),
tx.stagedAvailabilityRule.findMany({ where: { importBatchId: validation.batchId } }),
tx.user.findFirst({ where: { systemRole: "ADMIN" }, select: { id: true } }),
tx.country.findMany({ select: { id: true, code: true } }),
tx.metroCity.findMany({ select: { id: true, name: true } }),
tx.managementLevelGroup.findMany({
select: { id: true, name: true, targetPercentage: true },
}),
tx.managementLevel.findMany({ select: { id: true, name: true } }),
tx.client.findMany({ select: { id: true, code: true, name: true } }),
tx.orgUnit.findMany({ select: { id: true, level: true, name: true } }),
tx.role.findMany({ select: { id: true, name: true } }),
tx.utilizationCategory.findMany({ select: { id: true, code: true } }),
]);
if (!adminUser) {
throw new Error("Cannot commit Dispo import without an ADMIN user in the database");
}
const existing = await tx.vacation.findFirst({
where: {
endDate: stagedVacation.endDate, halfDayPart: stagedVacation.halfDayPart,
isHalfDay: stagedVacation.isHalfDay, note: stagedVacation.note, resourceId,
startDate: stagedVacation.startDate, status: VacationStatus.APPROVED,
type: stagedVacation.vacationType,
},
select: { id: true },
const maps = buildReferenceDataMaps({
clients,
countries,
managementLevelGroups,
managementLevels,
metroCities,
orgUnits,
roles,
utilizationCategories,
});
if (existing) {
await tx.vacation.update({
where: { id: existing.id },
data: { approvedAt: new Date(), approvedById: adminUser.id, note: stagedVacation.note, requestedById: adminUser.id },
const mergedResources = mergeStagedResources(stagedResources);
await ensureInferredRolesExist(tx, mergedResources, maps.roleIdByName);
const resourceResult = await commitResources(
tx,
mergedResources,
maps,
validation.batchId,
stagedVacations,
stagedAvailabilityRules,
);
const projectIdByShortCode = await commitProjects(
tx,
stagedProjects,
maps,
validation.batchId,
input.importTbdProjects ?? false,
);
// Commit assignments
const aggregatedAssignments = aggregateAssignments(
stagedAssignments,
resourceResult.resourceIdByKey,
projectIdByShortCode,
maps.roleIdByName,
resourceResult.resourceRoleNameByKey,
input.importTbdProjects ?? false,
);
for (const assignment of aggregatedAssignments) {
const dailyCostCents = Math.round(
assignment.hoursPerDay * (mergedResources.get(assignment.resourceKey)?.lcrCents ?? 0),
);
const metadata = {
dispoImport: {
importBatchId: validation.batchId,
sourceDates: assignment.sourceDates,
utilizationCategoryCode: assignment.utilizationCategoryCode,
},
} as Prisma.InputJsonValue;
await tx.assignment.upsert({
where: {
unique_assignment: {
resourceId: assignment.resourceId,
projectId: assignment.projectId,
startDate: assignment.startDate,
endDate: assignment.endDate,
},
},
update: {
dailyCostCents,
metadata,
percentage: assignment.percentage,
role: assignment.roleName,
roleId: assignment.roleId,
status: AllocationStatus.PROPOSED,
},
create: {
dailyCostCents,
metadata,
endDate: assignment.endDate,
hoursPerDay: assignment.hoursPerDay,
percentage: assignment.percentage,
projectId: assignment.projectId,
resourceId: assignment.resourceId,
role: assignment.roleName,
roleId: assignment.roleId,
startDate: assignment.startDate,
status: AllocationStatus.PROPOSED,
},
});
} else {
await tx.vacation.create({
data: {
approvedAt: new Date(), approvedById: adminUser.id,
endDate: stagedVacation.endDate, halfDayPart: stagedVacation.halfDayPart,
isHalfDay: stagedVacation.isHalfDay, note: stagedVacation.note,
requestedById: adminUser.id, resourceId,
startDate: stagedVacation.startDate, status: VacationStatus.APPROVED,
await tx.resourceRole.upsert({
where: {
resourceId_roleId: { resourceId: assignment.resourceId, roleId: assignment.roleId },
},
update: {},
create: { resourceId: assignment.resourceId, roleId: assignment.roleId },
});
resourceResult.upsertedResourceRoles += 1;
}
// Commit vacations
for (const stagedVacation of stagedVacations) {
const resourceId = resourceResult.resourceIdByKey.get(stagedVacation.resourceExternalId);
if (!resourceId) {
throw new Error(
`Unable to resolve resource "${stagedVacation.resourceExternalId}" during vacation commit`,
);
}
const existing = await tx.vacation.findFirst({
where: {
endDate: stagedVacation.endDate,
halfDayPart: stagedVacation.halfDayPart,
isHalfDay: stagedVacation.isHalfDay,
note: stagedVacation.note,
resourceId,
startDate: stagedVacation.startDate,
status: VacationStatus.APPROVED,
type: stagedVacation.vacationType,
},
select: { id: true },
});
if (existing) {
await tx.vacation.update({
where: { id: existing.id },
data: {
approvedAt: new Date(),
approvedById: adminUser.id,
note: stagedVacation.note,
requestedById: adminUser.id,
},
});
} else {
await tx.vacation.create({
data: {
approvedAt: new Date(),
approvedById: adminUser.id,
endDate: stagedVacation.endDate,
halfDayPart: stagedVacation.halfDayPart,
isHalfDay: stagedVacation.isHalfDay,
note: stagedVacation.note,
requestedById: adminUser.id,
resourceId,
startDate: stagedVacation.startDate,
status: VacationStatus.APPROVED,
type: stagedVacation.vacationType,
},
});
}
}
}
// Mark staged records as committed
await Promise.all([
tx.stagedResource.updateMany({ where: { importBatchId: validation.batchId }, data: { status: StagedRecordStatus.COMMITTED } }),
tx.stagedProject.updateMany({
where: input.importTbdProjects ? { importBatchId: validation.batchId } : { importBatchId: validation.batchId, isTbd: false },
data: { status: StagedRecordStatus.COMMITTED },
}),
tx.stagedAssignment.updateMany({
where: input.importTbdProjects
? { importBatchId: validation.batchId, isUnassigned: false }
: { importBatchId: validation.batchId, isTbd: false, isUnassigned: false },
data: { status: StagedRecordStatus.COMMITTED },
}),
tx.stagedVacation.updateMany({ where: { importBatchId: validation.batchId }, data: { status: StagedRecordStatus.COMMITTED } }),
tx.stagedAvailabilityRule.updateMany({ where: { importBatchId: validation.batchId }, data: { status: StagedRecordStatus.COMMITTED } }),
]);
await tx.importBatch.update({
where: { id: validation.batchId },
data: {
committedAt: new Date(),
status: ImportBatchStatus.COMMITTED,
summary: buildBatchSummaryEntry({
...toJsonObject(validation.batchSummary),
commit: {
committedAssignments: aggregatedAssignments.length,
committedProjects: projectIdByShortCode.size,
committedResources: mergedResources.size,
committedVacations: stagedVacations.length,
skippedTbdUnresolved: validation.skippedTbdUnresolved,
updatedEntitlements: resourceResult.updatedEntitlements,
updatedResourceAvailabilities: resourceResult.updatedResourceAvailabilities,
upsertedResourceRoles: resourceResult.upsertedResourceRoles,
},
// Mark staged records as committed
await Promise.all([
tx.stagedResource.updateMany({
where: { importBatchId: validation.batchId },
data: { status: StagedRecordStatus.COMMITTED },
}),
},
});
tx.stagedProject.updateMany({
where: input.importTbdProjects
? { importBatchId: validation.batchId }
: { importBatchId: validation.batchId, isTbd: false },
data: { status: StagedRecordStatus.COMMITTED },
}),
tx.stagedAssignment.updateMany({
where: input.importTbdProjects
? { importBatchId: validation.batchId, isUnassigned: false }
: { importBatchId: validation.batchId, isTbd: false, isUnassigned: false },
data: { status: StagedRecordStatus.COMMITTED },
}),
tx.stagedVacation.updateMany({
where: { importBatchId: validation.batchId },
data: { status: StagedRecordStatus.COMMITTED },
}),
tx.stagedAvailabilityRule.updateMany({
where: { importBatchId: validation.batchId },
data: { status: StagedRecordStatus.COMMITTED },
}),
]);
return {
batchId: validation.batchId,
counts: {
committedAssignments: aggregatedAssignments.length,
committedProjects: projectIdByShortCode.size,
committedResources: mergedResources.size,
committedVacations: stagedVacations.length,
updatedEntitlements: resourceResult.updatedEntitlements,
updatedResourceAvailabilities: resourceResult.updatedResourceAvailabilities,
upsertedResourceRoles: resourceResult.upsertedResourceRoles,
},
unresolved: { blocked: 0, skippedTbd: validation.skippedTbdUnresolved },
} satisfies CommitDispoImportBatchResult;
}, { maxWait: 30_000, timeout: 600_000 });
await tx.importBatch.update({
where: { id: validation.batchId },
data: {
committedAt: new Date(),
status: ImportBatchStatus.COMMITTED,
summary: buildBatchSummaryEntry({
...toJsonObject(validation.batchSummary),
commit: {
committedAssignments: aggregatedAssignments.length,
committedProjects: projectIdByShortCode.size,
committedResources: mergedResources.size,
committedVacations: stagedVacations.length,
skippedTbdUnresolved: validation.skippedTbdUnresolved,
updatedEntitlements: resourceResult.updatedEntitlements,
updatedResourceAvailabilities: resourceResult.updatedResourceAvailabilities,
upsertedResourceRoles: resourceResult.upsertedResourceRoles,
},
}),
},
});
return {
batchId: validation.batchId,
counts: {
committedAssignments: aggregatedAssignments.length,
committedProjects: projectIdByShortCode.size,
committedResources: mergedResources.size,
committedVacations: stagedVacations.length,
updatedEntitlements: resourceResult.updatedEntitlements,
updatedResourceAvailabilities: resourceResult.updatedResourceAvailabilities,
upsertedResourceRoles: resourceResult.upsertedResourceRoles,
},
unresolved: { blocked: 0, skippedTbd: validation.skippedTbdUnresolved },
} satisfies CommitDispoImportBatchResult;
},
{ maxWait: 30_000, timeout: 600_000 },
);
if ("resource" in db && db.resource) {
await recomputeResourceValueScores(db);