fix(ui+alerts+email): prod walkthrough batch
All checks were successful
Build & Push Docker Images / lint (push) Successful in 3m14s
Build & Push Docker Images / build-and-push (push) Successful in 9m10s

- proxy/banner: interest-berth-status-banner used the interest_berths
  junction id for /api/v1/berths/{id}/active-interests (404 on every
  interest with a sold/under-offer berth). Add berthId to BerthRow and use
  it for both the active-interests query and the BerthOccupancyChip.
- scroll-area: override Radix viewport `display:table` (`[&>div]:!block`) so
  content respects the viewport width — fixes notification alert cards
  overflowing past the popover. No horizontal-scroll ScrollArea in the app.
- alert-card: drop the raw `interest.stale` rule key from the footer
  (plaintext only; the title already conveys the alert).
- alert-rules (interest.stale): add a createdAt >14d floor so a bulk import
  that backdates dateLastContact doesn't instantly flag every migrated
  interest as stale and flood the alert rail. 14-day clock starts no earlier
  than when the interest entered this system.
- env: allow EMAIL_REDIRECT_TO in production behind an explicit
  ALLOW_PROD_EMAIL_REDIRECT=true opt-in (beta: route all outbound mail to
  the operator inbox; default still refuses the footgun).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 14:09:16 +02:00
parent 72028a7f32
commit 93c6554c95
5 changed files with 35 additions and 10 deletions

View File

@@ -49,8 +49,18 @@ const envSchema = z
SMTP_PASS: z.string().optional(),
SMTP_FROM: z.string().optional(),
// Dev/test safety net: when set, sendEmail redirects every outbound message
// to this address regardless of the requested recipient. Leave empty in prod.
// to this address regardless of the requested recipient. Leave empty in prod
// UNLESS deliberately enabling the beta redirect (see ALLOW_PROD_EMAIL_REDIRECT).
EMAIL_REDIRECT_TO: z.string().email().optional(),
// Explicit opt-in to allow EMAIL_REDIRECT_TO in production. The prod guard
// below normally refuses to boot with a redirect set (footgun: silently
// funnels all customer mail to one inbox). During beta we WANT that — every
// outbound message goes to the operator inbox so nothing reaches real
// clients by accident. Must be set together with EMAIL_REDIRECT_TO.
ALLOW_PROD_EMAIL_REDIRECT: z
.enum(['true', 'false'])
.default('false')
.transform((v) => v === 'true'),
// Encryption
EMAIL_CREDENTIAL_KEY: z
@@ -118,13 +128,14 @@ const envSchema = z
// funnels every customer email (invites, EOIs, portal magic links,
// contracts) to a single inbox. The audit caught this had only a
// `logger.debug` line as forensic trail. Refuse boot when both are
// simultaneously set in production.
if (env.NODE_ENV === 'production' && env.EMAIL_REDIRECT_TO) {
// simultaneously set in production - UNLESS ALLOW_PROD_EMAIL_REDIRECT=true
// is set as a deliberate opt-in (beta: route all mail to the operator).
if (env.NODE_ENV === 'production' && env.EMAIL_REDIRECT_TO && !env.ALLOW_PROD_EMAIL_REDIRECT) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['EMAIL_REDIRECT_TO'],
message:
'EMAIL_REDIRECT_TO must NOT be set in production - it silently rewrites every outbound email recipient. Unset it before deploying.',
'EMAIL_REDIRECT_TO must NOT be set in production - it silently rewrites every outbound email recipient. Unset it, or set ALLOW_PROD_EMAIL_REDIRECT=true to deliberately enable it (e.g. beta).',
});
}
});

View File

@@ -99,6 +99,12 @@ async function interestStale(portId: string): Promise<AlertCandidate[]> {
eq(interests.portId, portId),
inArray(interests.pipelineStage, STALE_STAGES),
isNull(interests.archivedAt),
// An interest can't be "stale for 14+ days" if it has only existed for
// less than 14 days. Without this floor, a bulk import (which backdates
// dateLastContact to the legacy value) instantly flags every migrated
// interest as stale and floods the alert rail. The 14-day clock starts
// no earlier than when the interest entered THIS system.
lt(interests.createdAt, daysAgo(14)),
or(
lt(interests.dateLastContact, daysAgo(14)),
and(isNull(interests.dateLastContact), lt(interests.updatedAt, daysAgo(14))),