fix(v2): critical bug fixes — root SSH, bootstrap wait, SG deletion order, PAT masking

- EC2 bootstrap now enables root SSH (copies authorized_keys to root, sets
  PermitRootLogin without-password, reloads sshd) so all commands run as
  root and NVM at /root/.nvm is accessible
- Added /var/lib/pp-bootstrap-done sentinel; deploy waits for it before
  running any user commands — prevents race between SSH availability and
  user-data completion (docker/nvm install can take 3-5+ min)
- Fixed stopPreview: terminate instance first, delete key pair next, then
  delete security group with 30s delay — SG deletion was previously
  attempted before termination causing it to fail
- Fixed redeploy to always fetch fresh instanceIp/sshPrivateKey from DB
  rather than using potentially-stale preview parameter
- Fixed .env writing to use base64 encoding via echo|base64-d to safely
  handle values with special characters, single quotes, and newlines
- PAT and git clone URL now masked in preview logs (shows **** for password)
- Fixed inactivity cron: removed dead inactivityMs variable, use join on
  repoConfig to avoid N+1, deduplicate pending INACTIVITY_STOP jobs
- Fixed IAM policy UI: ec2:CreateKeyPair (backend uses CreateKeyPair, not
  ImportKeyPair which is a different AWS operation)
- Admin panel: added Edit button with username/password form for users
- Privacy page: fetch and display admin contactEmail from settings
- pnpm-workspace.yaml: fix allowBuilds→onlyBuiltDependencies for pnpm 9

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 08:34:50 +00:00
parent f974840868
commit 25f3612561
9 changed files with 211 additions and 81 deletions
+17 -14
View File
@@ -28,29 +28,32 @@ export function startCronWorkers() {
}
async function checkInactivity() {
const settings = await getAdminSettings();
const now = new Date();
const running = await prisma.preview.findMany({
where: { status: "RUNNING" },
include: { repoConfig: { select: { inactivityHours: true } } },
});
for (const preview of running) {
const inactivityMs = settings.maxConcurrentInstancesPerUser; // will use actual inactivityHours from repoConfig
const repoConfig = await prisma.repoConfig.findUnique({ where: { id: preview.repoConfigId } });
if (!repoConfig) continue;
const deadline = new Date(preview.lastActivityAt.getTime() + repoConfig.inactivityHours * 3600 * 1000);
const inactivityHours = preview.repoConfig.inactivityHours;
const deadline = new Date(preview.lastActivityAt.getTime() + inactivityHours * 3600 * 1000);
if (now >= deadline) {
log.info({ previewId: preview.id }, "Preview inactive, enqueuing INACTIVITY_STOP");
await prisma.job.create({
data: {
previewId: preview.id,
type: "INACTIVITY_STOP",
status: "PENDING",
payload: {},
},
log.info({ previewId: preview.id, inactivityHours }, "Preview inactive, enqueuing INACTIVITY_STOP");
// Only create one pending INACTIVITY_STOP per preview
const existingStop = await prisma.job.findFirst({
where: { previewId: preview.id, type: "INACTIVITY_STOP", status: { in: ["PENDING", "RUNNING"] } },
});
if (!existingStop) {
await prisma.job.create({
data: {
previewId: preview.id,
type: "INACTIVITY_STOP",
status: "PENDING",
payload: {},
},
});
}
}
}
}