[codex] Publish app admin social updates#113
Conversation
- Admin social account profile + dashboard load-time instrumentation - New media-mirror drain/recover, comments repair-auth, social-growth, and completion-summary API routes; media-queue snapshots - shadcn-style UI primitives (alert/badge/card/input/separator), trr-app flags - Vercel analytics + speed-insights in root layout; allowedDevOrigins - postgres pool connection-string resolver; portless/api-default updates; tests
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 29 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThis PR bundles many independent changes: Portless/admin host URL routing simplification, dev-dashboard Vercel/Portless readiness widgets, new Instagram catalog/comments mockup dashboards, a social media queue ingestion UI with backend routes, expanded social account profile types/API routes, relay-checkpoint comments recovery, SocialBlade refresh polling, unified show auto-refresh, Reddit alias redirects, new UI primitives, shared live-resource polling fixes, brand font scoring refactor, timezone/growth utilities, and dependency/script updates. ChangesAdmin Host Routing & Dashboards
Social Ingestion, Profile API & Recovery
Other Areas
Estimated code review effort: 5 (Critical) | ~150 minutes Sequence Diagram(s)sequenceDiagram
participant Operator
participant CastSocialBladeComparison
participant RefreshBatchAPI
participant ModalCallStatusAPI
Operator->>CastSocialBladeComparison: trigger batch refresh
CastSocialBladeComparison->>RefreshBatchAPI: POST /refresh-batch
RefreshBatchAPI-->>CastSocialBladeComparison: accepted entries with callId
loop polling
CastSocialBladeComparison->>ModalCallStatusAPI: fetch call status by callId
ModalCallStatusAPI-->>CastSocialBladeComparison: queued/running/completed/failed
end
CastSocialBladeComparison-->>Operator: render status rows and history
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codex Exhaustive Code ReviewFindings
Validation Reviewed only |
There was a problem hiding this comment.
Pull request overview
Publishes the next iteration of admin social updates (UI + supporting API routes), while keeping higher-risk ingestion/operator controls gated behind explicit admin-social flags and improving local-dev diagnostics around Portless/Vercel tooling.
Changes:
- Adds new admin social proxy routes + media-queue snapshot listing/reading, with accompanying unit/E2E coverage.
- Introduces centralized
trr-app-flagsfor safe-by-default admin/social feature gating. - Enhances local dev-dashboard diagnostics (Portless status, Vercel preview readiness + nested-link doctor) and refines admin-host proxy behavior.
frontend_surface
- Admin social pages/components, admin host proxy middleware behavior, and dev-dashboard diagnostics UI/services.
rendering_boundaries
- Client components: new/updated admin UI surfaces and shared polling behavior.
- Route handlers: multiple new
/api/admin/...handlers for social/admin proxying and workspace snapshot reads. - Middleware/proxy:
apps/web/src/proxy.tsrouting/rewrites.
contract_dependencies
- TRR-Backend admin social endpoints (social proxy paths, SocialBlade admin endpoints, ingest/media-mirror endpoints).
- New public env flags:
NEXT_PUBLIC_TRR_FLAG_ADMIN_SOCIAL_*(and existing admin social flags). - Local workspace artifacts under
.logs/workspace/...for snapshot/readiness tooling.
validation_run
git diff --check origin/main...HEADpnpm -C apps/web exec vitest run tests/admin-social-page-auth-bypass.test.tsxpnpm -C apps/web exec tsc -p tsconfig.typecheck.json --noEmitpnpm run web:validate:quick
residual_ui_risks
Badgeis missing"use client"and likely misuses Radix Slot (Slot.Root), which can cause Next.js build/runtime failures and/or runtime crashes whenasChildis used (blocking).live-profile-totalroute currently masks all upstream failures as a 200 degraded payload; consider narrowing fallback behavior to timeout/unreachable classes to avoid hiding real upstream 4xx/5xx errors.
Reviewed changes
Copilot reviewed 89 out of 92 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/test-vercel-guard.sh | Adds more complete guard-only and explicit-prod opt-in coverage for Vercel wrapper behavior. |
| scripts/portless-banner.mjs | Updates Portless banner URLs to treat clean admin root as the canonical entry. |
| pnpm-workspace.yaml | Adds overrides and release-age exclusions for specific dependency versions. |
| package.json | Bumps Firebase/Firebase Admin and firebase-tools versions at repo root. |
| apps/web/tests/trr-app-flags.test.ts | Adds unit coverage for the new centralized app flags module. |
| apps/web/tests/social-media-queue-snapshots-route.test.ts | Tests media-queue snapshot listing and safe filename validation. |
| apps/web/tests/social-ingest-media-mirror-recover-route.test.ts | Tests recover-stale media-mirror proxy route validation + forwarding. |
| apps/web/tests/social-ingest-media-mirror-drain-account-route.test.ts | Tests drain-account proxy route validation + forwarding. |
| apps/web/tests/social-growth-cookie-health-route.test.ts | Tests SocialBlade cookie health proxy behavior and error propagation. |
| apps/web/tests/social-account-profile-snapshot-route.test.ts | Updates snapshot expectations around progress timeout + caching behavior. |
| apps/web/tests/social-account-profile-page.hashtag-preview.test.ts | Adjusts hashtag preview behavior test inputs for new assignment status param. |
| apps/web/tests/social-account-profile-completion-summary-route.test.ts | Adds route tests for completion summary backed by Postgres query. |
| apps/web/tests/social-account-profile-catalog-diagnostics.test.ts | Adds unit coverage for catalog progress diagnostics rows. |
| apps/web/tests/social-account-profile-backfill-command.test.ts | Updates backfill selected tasks defaults to include media. |
| apps/web/tests/social-account-live-profile-total-route.test.ts | Adds test for fast degraded fallback behavior on live total timeout/error. |
| apps/web/tests/social-account-catalog-run-progress-route.test.ts | Updates fast polling timeout expectations to use progress timeout constant. |
| apps/web/tests/social-account-catalog-recent-runs-route.test.ts | Adds tests for recent catalog runs route and attached followup coercion. |
| apps/web/tests/social-account-catalog-alt-page.test.ts | Adds tests for the new catalog alt-1 page routing/canonicalization. |
| apps/web/tests/show-refresh-health-center-wiring.test.ts | Ensures show detail page auto-refresh wiring is present and stable. |
| apps/web/tests/shared-live-resource-polling.test.tsx | Adds test to prevent duplicate polls when multiple subscribers refetch in-flight. |
| apps/web/tests/postgres-connection-string-resolution.test.ts | Adds coverage for stripping pg SSL query params in pool connection string. |
| apps/web/tests/instagram-catalog-backfill-mockup-page.test.tsx | Adds component-level tests for the Instagram catalog backfill mockup page. |
| apps/web/tests/finish.flow.test.tsx | Adds Finish page tests for API-provided show list + fallback behavior. |
| apps/web/tests/e2e/admin-fixtures.ts | Expands mocked admin API to support social growth + season social routes. |
| apps/web/tests/e2e/admin-cast-tabs-smoke.spec.ts | Adds E2E smoke for SocialBlade refresh modal queued/running call id behavior. |
| apps/web/tests/dev-dashboard-vercel-preview-readiness-service.test.ts | Adds tests for reading preview-readiness artifact and nested-link doctor. |
| apps/web/tests/covered-shows-page-no-fanout-wiring.test.ts | Locks in show auto-refresh target fanout + navigation param behavior. |
| apps/web/tests/cast-socialblade-snapshot-wiring.test.ts | Wiring tests for polling + modal refresh status tracking and e2e alignment. |
| apps/web/tests/brand-font-matching-scoring.test.ts | Adds tests for scoring maxima extraction + null glyph artifact handling. |
| apps/web/tests/admin-social-page-auth-bypass.test.tsx | Extends admin social auth bypass test to cover new ingestion/media queue controls behind flags. |
| apps/web/tests/admin-portless-status.test.ts | Adds parsing coverage for portless service status output + route expectations. |
| apps/web/tests/admin-host-middleware.test.ts | Updates middleware tests for rewrite vs redirect behavior and legacy URL redirects. |
| apps/web/tests/admin-api-references-generator.test.ts | Ensures dynamic query templates normalize without leaking template sources. |
| apps/web/src/proxy.ts | Refines admin host proxy parsing/rewrites, adds API passthrough, and supports catalog alt variants. |
| apps/web/src/lib/trr-app-flags.ts | Introduces a single module for app/admin feature flags with safe defaults + parsing. |
| apps/web/src/lib/server/postgres.ts | Adds pg pool connection string sanitizer to avoid SSL query overriding explicit SSL config. |
| apps/web/src/lib/server/admin/portless-status.ts | Adds Portless service-status parsing and includes it in status snapshot. |
| apps/web/src/lib/server/admin/dev-dashboard-service.ts | Adds Vercel preview readiness + cleanup doctor data to dev dashboard service. |
| apps/web/src/lib/fonts/brand-fonts/scoring.ts | Refactors scoring profiles into shared module and propagates visual diagnostics in results. |
| apps/web/src/lib/fonts/brand-fonts/scoring-profiles.ts | New shared scoring profile constants/maxima for UI + tests. |
| apps/web/src/lib/data/shows.ts | Extends local show list with additional shows used in Finish flow. |
| apps/web/src/lib/admin/shared-live-resource.ts | Improves poll request merging and avoids duplicate polling while requests are in-flight. |
| apps/web/src/lib/admin/api-references/generator.ts | Normalizes/strips query-template fragments from generated path patterns. |
| apps/web/src/lib/admin/admin-url-defaults.ts | Centralizes Portless route definitions/links/status routes and health URL constants. |
| apps/web/src/components/ui/separator.tsx | Adds separator UI primitive. |
| apps/web/src/components/ui/input.tsx | Adds input UI primitive. |
| apps/web/src/components/ui/card.tsx | Adds card UI primitives used by admin mockups/views. |
| apps/web/src/components/ui/badge.tsx | Adds badge UI primitive used by admin mockups/views. |
| apps/web/src/components/ui/alert.tsx | Adds alert UI primitives used by admin mockups/views. |
| apps/web/src/components/admin/social-week/WeekDetailPageView.tsx | Adjusts snapshot polling gating to avoid unnecessary polling during comment syncing transitions. |
| apps/web/src/components/admin/design-system/BrandFontMatchesPanel.tsx | Uses shared scoring maxima to render score breakdown bars consistently. |
| apps/web/src/components/admin/cast-content-section.tsx | Allows dev admin bypass when fetching cast-role members in admin UI flows. |
| apps/web/src/components/admin/AdminGlobalHeader.tsx | Updates Portless fallback warning messaging and canonical admin URL reference. |
| apps/web/src/app/social/[platform]/[handle]/catalog/alt-1/page.tsx | Adds catalog alt-1 page variant with canonicalization/redirect behavior. |
| apps/web/src/app/layout.tsx | Adds Vercel Analytics + Speed Insights to root layout. |
| apps/web/src/app/auth/finish/page.tsx | Uses API-provided show list when available, falling back to local list. |
| apps/web/src/app/api/admin/trr-api/social/profiles/[platform]/[handle]/snapshot/route.ts | Adjusts progress timeout constant usage and removes forced refresh-on-active-work behavior. |
| apps/web/src/app/api/admin/trr-api/social/profiles/[platform]/[handle]/live-profile-total/route.ts | Adds fast timeout and degraded fallback payload behavior for live totals. |
| apps/web/src/app/api/admin/trr-api/social/profiles/[platform]/[handle]/comments/runs/[runId]/repair-auth/route.ts | Adds proxy route to repair comments-run auth via backend. |
| apps/web/src/app/api/admin/trr-api/social/profiles/[platform]/[handle]/comments/runs/[runId]/public-recovery/route.ts | Adds proxy route to start public recovery for a comments run. |
| apps/web/src/app/api/admin/trr-api/social/profiles/[platform]/[handle]/comments/runs/[runId]/authenticated-followup/route.ts | Adds proxy route to start authenticated follow-up for a comments run. |
| apps/web/src/app/api/admin/trr-api/social/profiles/[platform]/[handle]/catalog/runs/recent/route.ts | Adds Postgres-backed recent runs route with caching and attached followup normalization. |
| apps/web/src/app/api/admin/trr-api/social/profiles/[platform]/[handle]/catalog/runs/[runId]/progress/route.ts | Simplifies progress timeout selection and reporting to use shared progress timeout. |
| apps/web/src/app/api/admin/trr-api/social/ingest/media-mirror/recover-stale/route.ts | Adds validated proxy route for run-scoped stale media recovery. |
| apps/web/src/app/api/admin/trr-api/social/ingest/media-mirror/drain-account/route.ts | Adds validated proxy route for run-scoped account drain operations. |
| apps/web/src/app/api/admin/trr-api/social-growth/history/route.ts | Adds internal-admin-auth SocialBlade history proxy route. |
| apps/web/src/app/api/admin/trr-api/social-growth/cookies/health/route.ts | Adds internal-admin-auth SocialBlade cookie health proxy route. |
| apps/web/src/app/api/admin/trr-api/social-growth/calls/[callId]/route.ts | Adds internal-admin-auth SocialBlade call-status proxy route. |
| apps/web/src/app/api/admin/social/media-queue/snapshots/route.ts | Adds admin-only API for listing/reading media queue snapshot artifacts safely. |
| apps/web/src/app/admin/trr-shows/page.tsx | Expands auto-refresh fanout targets and propagates pre-navigation refresh signals. |
| apps/web/src/app/admin/trr-shows/[showId]/page.tsx | Switches to auto “full refresh once per show view” logic with sessionStorage guard. |
| apps/web/src/app/admin/page.tsx | Updates Portless URL card to use centralized route defs and richer diagnostics snapshot. |
| apps/web/src/app/admin/dev-dashboard/instagram-catalog-backfill-mockup/page.tsx | Adds a dedicated dev-dashboard mockup page for catalog backfill UI. |
| apps/web/src/app/admin/dev-dashboard/_components/DevDashboardShell.tsx | Adds nav entry for the new catalog backfill dev-dashboard page. |
| apps/web/scripts/perf-admin-api.mjs | Adds a local perf harness script for low-concurrency admin API benchmarking. |
| apps/web/README.md | Updates documentation to point to clean Portless admin root URL. |
| apps/web/playwright.config.ts | Aligns base URL/default webServer env to use localhost rather than 127.0.0.1. |
| apps/web/package.json | Adds Vercel analytics deps, bumps several deps/devDeps, adds perf script. |
| apps/web/next.config.ts | Adds allowedDevOrigins for local dev hostnames. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import * as React from "react" | ||
| import { cva, type VariantProps } from "class-variance-authority" | ||
| import { Slot } from "radix-ui" | ||
|
|
| ...props | ||
| }: React.ComponentProps<"span"> & | ||
| VariantProps<typeof badgeVariants> & { asChild?: boolean }) { | ||
| const Comp = asChild ? Slot.Root : "span" |
| return NextResponse.json(buildLiveProfileTotalFallback(platform, handle, "backend_timeout_or_error"), { | ||
| headers: { | ||
| "x-trr-cache": "fallback", | ||
| "x-trr-cacheable": "0", | ||
| }, |
There was a problem hiding this comment.
Actionable comments posted: 16
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/src/components/admin/instagram/InstagramCommentsPanel.tsx (1)
869-900: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick winStop the relay-checkpoint auto-refresh from re-running on every filter keystroke. The auto-load effect depends on
refreshRelayCheckpointRecovery, and that callback depends onrelayCheckpointShowFilter, so typing in the filter issues a fetch per character and can let stale responses overwrite newer state. Keep the mount/auto-refresh path stable and read the latest filter only from the manual Refresh action.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/components/admin/instagram/InstagramCommentsPanel.tsx` around lines 869 - 900, The relay-checkpoint auto-refresh is re-running on every `relayCheckpointShowFilter` keystroke because `refreshRelayCheckpointRecovery` is recreated from that state and is used by the `useEffect` auto-load path. Make the effect stable so it only auto-loads on mount/support changes, and remove the filter from the auto-refresh dependency chain in `InstagramCommentsPanel` while still reading the latest filter when the user explicitly clicks Refresh. Keep the fetch logic in `refreshRelayCheckpointRecovery` and the `useEffect` trigger separated so stale responses from intermediate keystrokes cannot overwrite newer state.
🟡 Minor comments (8)
apps/web/src/components/admin/instagram/InstagramCommentsPanel.tsx-1714-1721 (1)
1714-1721: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winShow-filter input has no accessible name.
This input only carries a
placeholder, which is not a reliable accessible name for assistive tech (the main posts search at Line 1994-1995 uses ansr-onlylabel). Add anaria-label(or associated<label>).🛠 Proposed fix
<input type="search" + aria-label="Filter relay checkpoint targets by show" value={relayCheckpointShowFilter} onChange={(event) => setRelayCheckpointShowFilter(event.target.value)} placeholder="Show filter"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/components/admin/instagram/InstagramCommentsPanel.tsx` around lines 1714 - 1721, The show-filter search field in InstagramCommentsPanel lacks an accessible name because it only relies on a placeholder. Update the input bound to relayCheckpointShowFilter to include an aria-label or associate it with a visible/sr-only label, matching the approach used by the main posts search in this component. Keep the existing behavior and styling intact while ensuring the input is properly announced by assistive technologies.apps/web/src/lib/server/admin/dev-dashboard-service.ts-175-183 (1)
175-183: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winHandle
.verceldetection with path components.root.endsWith("/.vercel")is POSIX-only, socollectVercelProjectFiles()will miss valid project links on Windows. Use a path-segment check instead.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/lib/server/admin/dev-dashboard-service.ts` around lines 175 - 183, The `.vercel` project-root check in `collectVercelProjectFiles()` is using a POSIX-only suffix match, so it can miss valid links on Windows. Update the path detection logic to inspect path components via the existing path utilities instead of relying on `root.endsWith("/.vercel")`, and keep the behavior localized to the `collectVercelProjectFiles` flow.apps/web/src/app/api/admin/trr-api/social-growth/calls/[callId]/route.ts-24-24 (1)
24-24: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick winForward the admin context to the backend.
requireAdmin(request)returns the authenticated admin, butbuildInternalAdminHeaders()is called without that context here, so the outbound request omits the admin identity metadata. Pass aVerifiedAdminContextthrough so SocialBlade actions can be attributed server-side.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/app/api/admin/trr-api/social-growth/calls/`[callId]/route.ts at line 24, The admin identity is not being forwarded to the backend because the result of requireAdmin(request) is not passed into buildInternalAdminHeaders() in the [callId] route handler. Update the handler to capture the VerifiedAdminContext returned by requireAdmin(request), then pass that context into buildInternalAdminHeaders so the outbound SocialBlade request includes the admin metadata for server-side attribution.apps/web/src/app/admin/dev-dashboard/instagram-comments-mockup/page.tsx-45-45 (1)
45-45: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win"Overview" sidebar link points to a non-existent anchor.
navItems[0]("Overview") slugifies to#overview, but no element in the page hasid="overview"— onlyactive-run(Line 529),workers(Line 612),coverage(Line 676), andactions(Line 575) exist. Clicking "Overview" does nothing.Proposed fix
- <section className="px-4 py-5 sm:px-6 lg:px-8"> + <section id="overview" className="px-4 py-5 sm:px-6 lg:px-8">Also applies to: 457-469, 479-479
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/app/admin/dev-dashboard/instagram-comments-mockup/page.tsx` at line 45, The first entry in navItems (“Overview”) points to a missing anchor, so the sidebar link cannot navigate anywhere. Update the dev dashboard page’s section anchors or nav mapping in instagram-comments-mockup/page.tsx so the “Overview” item targets an actual element id, or rename/remove it if there is no corresponding section. Verify the nav item generation and the section headings for active-run, workers, coverage, and actions stay aligned.apps/web/src/app/admin/dev-dashboard/instagram-comments-mockup/page.tsx-84-87 (1)
84-87: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
readFiniteNumbertreats explicitnullas0instead of "no data".
Number(null)evaluates to0, which is finite, so an explicitnull(a common "not yet available" API signal) is silently coerced to0rather than propagating asnull. Downstream consumers likecompletedPosts !== null && totalPosts !== null(Lines 270-272, 363-366) rely onnullto distinguish "waiting for data" from "zero progress" — with this bug, a payload field explicitly set tonullwould render as0 of N posts checkedinstead ofWaiting for post progress, misrepresenting run state.Proposed fix
const readFiniteNumber = (value: unknown): number | null => { + if (value === null || value === undefined) return null; const numeric = Number(value); return Number.isFinite(numeric) ? numeric : null; };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/app/admin/dev-dashboard/instagram-comments-mockup/page.tsx` around lines 84 - 87, `readFiniteNumber` is coercing explicit null values to 0, which breaks the “no data yet” signal used by the dashboard. Update the helper in `page.tsx` so it returns null when the input is null or undefined before calling Number(), and keep the finite-number check for real numeric inputs. This will preserve the null semantics relied on by the progress rendering logic that distinguishes waiting states from zero progress.scripts/vercel.sh-429-434 (1)
429-434: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
latest.jsoncopy can fail for custom--output/TRR_VERCEL_PREVIEW_READY_OUTPUTpaths.
write_preview_ready_artifactonlymkdir -ps the directory of$output_path. The subsequentcp "$output_path" "$WORKSPACE_ROOT/.logs/workspace/vercel-preview-ready/latest.json"at Line 518 targets a hardcoded directory that is never created explicitly — it only exists if a prior default-path run created it. If a caller always supplies a custom--output/TRR_VERCEL_PREVIEW_READY_OUTPUT(e.g. in CI) and that directory has never been populated by a default run,cpfails and (assumingset -eis active at the top of the script, consistent with theset +e/set -etoggling pattern used elsewhere) the wholepreview-readycommand fails even though all three checks (Web Analytics, Speed Insights, deployments) succeeded.🐛 Proposed fix
+ mkdir -p "$WORKSPACE_ROOT/.logs/workspace/vercel-preview-ready" cp "$output_path" "$WORKSPACE_ROOT/.logs/workspace/vercel-preview-ready/latest.json"Also applies to: 517-519
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/vercel.sh` around lines 429 - 434, The preview-ready artifact flow in write_preview_ready_artifact can fail when copying to the hardcoded latest.json path because that directory is never created for custom output paths. Update the logic around write_preview_ready_artifact and the latest.json copy so the destination directory under .logs/workspace/vercel-preview-ready is created before cp runs, regardless of whether default_preview_ready_output or a custom --output/TRR_VERCEL_PREVIEW_READY_OUTPUT path was used.apps/web/scripts/perf-admin-api.mjs-92-97 (1)
92-97: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
--max-p95-msnever trips with autocannon JSON output.percentileMs(parsed, "p95")only looks forlatency.p95/latency.pp95, but autocannon emitsp2_5, p50, p75, p90, p97_5, p99, p99_9, p99_99, p99_999;p95is alwaysnull, so the max-p95 guard is skipped. Usep97_5here or rename the flag to match the available percentile.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/scripts/perf-admin-api.mjs` around lines 92 - 97, The max-p95 check in percentileMs is reading a percentile name that autocannon does not emit, so the guard in perf-admin-api.mjs never sees a value. Update the logic used by the max-p95 path to read the autocannon percentile that corresponds to p95 behavior, such as p97_5, or rename the flag to match the available percentile; keep the change localized around percentileMs and the code that calls it for --max-p95-ms.apps/web/src/app/admin/social/page.tsx-337-377 (1)
337-377: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
run.activefalsy-zero bug undercounts/overcounts active jobs.
toNonNegativeInt(run.active) || <sum of stage counts>treats a legitimate0returned fromrun.activethe same as "not provided", falling through to recomputeactivefrom stage buckets. If the backend reportsactive: 0while stale stage-bucket rows haven't been cleared yet, the badge/dropdown would show a non-zero "active" count for a run that's actually idle — misleading during recovery/drain decisions. Contrast withgetFirstNonNegativeIntelsewhere in this file, which correctly distinguishesnull/undefinedfrom an explicit0.🐛 Proposed fix
- const active = - toNonNegativeInt(run.active) || - SOCIAL_MEDIA_QUEUE_STAGES.reduce((total, stage) => { - const bucket = run.stages?.[stage]; - return ( - total + - queueBucketCount(bucket, "queued") + - queueBucketCount(bucket, "pending") + - queueBucketCount(bucket, "running") + - queueBucketCount(bucket, "retrying") - ); - }, 0); + const activeFromField = + run.active === null || run.active === undefined + ? null + : toNonNegativeInt(run.active); + const active = + activeFromField ?? + SOCIAL_MEDIA_QUEUE_STAGES.reduce((total, stage) => { + const bucket = run.stages?.[stage]; + return ( + total + + queueBucketCount(bucket, "queued") + + queueBucketCount(bucket, "pending") + + queueBucketCount(bucket, "running") + + queueBucketCount(bucket, "retrying") + ); + }, 0);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/app/admin/social/page.tsx` around lines 337 - 377, The active-count fallback in buildMediaQueueRunOptions incorrectly treats an explicit 0 from run.active as missing because it uses a falsy check, which can cause stale stage buckets to override a real idle state. Update the run.active handling to distinguish null/undefined from a valid zero, similar to getFirstNonNegativeInt in this file, and only fall back to summing SOCIAL_MEDIA_QUEUE_STAGES when run.active is not provided. Keep the fix localized to buildMediaQueueRunOptions and preserve the existing runId/status/timestamp logic.
🧹 Nitpick comments (19)
apps/web/tests/public-route-boundary.test.ts (1)
53-65: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winTest doesn't validate the actual redirect target format.
The assertions only check for
redirect(and import presence, not the constructed path. This test would pass even with the missingsprefix bug found in all four alias pages (seasonNumber should be encoded ass${seasonNumber}, not just${seasonNumber}). Consider asserting the literals${encodeURIComponent((or similar) pattern to lock in the canonical URL shape.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/tests/public-route-boundary.test.ts` around lines 53 - 65, The public-route-boundary test only checks that a redirect exists, so it misses regressions in the redirect target shape. Update the assertions in the public route shim test to verify the actual canonical season path format built by the alias pages, especially the `s${...}` prefix in the redirect target. Use the existing `LEGACY_ADMIN_REDDIT_SEASON_ROUTE_FILES` loop and tighten the `source` expectations so it matches the literal encoded `s${encodeURIComponent(...)}`-style pattern rather than only `redirect(`.apps/web/src/lib/server/admin/dev-dashboard-service.ts (2)
332-352: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winUnbounded recursive scan walks the entire app source tree on every dashboard load.
collectVercelProjectFilesrecurses into every non-pruned subdirectory ofTRR_APP_ROOT(only.git,node_modules,.next,dist,build,.turbo,.venv,__pycache__are excluded) looking for a.vercel/project.json, which in practice only lives at package roots (e.g.apps/web/.vercel). This means a full filesystem walk ofsrc/,tests/, etc. runs synchronously on everygetDevDashboardData()call with no caching.Consider capping recursion depth or only scanning known package roots (e.g.
apps/*,packages/*) instead of a full recursive walk.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/lib/server/admin/dev-dashboard-service.ts` around lines 332 - 352, The collectVercelProjectFiles scan in dev-dashboard-service is doing an unbounded recursive walk from TRR_APP_ROOT, which can traverse src, tests, and other non-package trees on every dashboard load. Update collectVercelProjectFiles (and its caller in getDevDashboardData) to limit the search scope by either capping recursion depth or only iterating known package roots such as apps/* and packages/*, while still finding .vercel/project.json via the existing collectVercelProjectFiles logic and VERCEL_SCAN_PRUNED_DIRS pruning.
1084-1096: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winNew readiness collectors bypass the existing
allSettledresilience pattern.
repoStatusesPromise/taskPlansPromiseare intentionally wrapped inPromise.allSettledso one failing collector doesn't blank the whole dashboard. The three new promises (portlessStatusPromise,vercelPreviewReadinessPromise,vercelCleanupDoctorPromise) are awaited via plainPromise.all, so if any of them ever rejects unexpectedly, the entiregetDevDashboardData()call fails and the repo/task data that would otherwise render successfully is lost too.The current implementations appear to internally catch their own errors, so this isn't actively broken today, but it's an inconsistency worth closing for defense-in-depth.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/lib/server/admin/dev-dashboard-service.ts` around lines 1084 - 1096, The new readiness collectors are not using the same resilience pattern as the existing dashboard collectors. Update getDevDashboardData() so portlessStatusPromise, vercelPreviewReadinessPromise, and vercelCleanupDoctorPromise are handled with allSettled-style protection like repoStatusesPromise and taskPlansPromise, then extract their values defensively instead of relying on plain Promise.all. Keep the existing symbols getPortlessStatus(), readVercelPreviewReadinessArtifact(), and readVercelCleanupDoctor grouped in the same guarded flow so one unexpected rejection does not fail the entire dashboard load.apps/web/tests/dev-dashboard-vercel-preview-readiness-service.test.ts (1)
1-117: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winGood coverage of the happy paths; consider adding negative-path tests.
Coverage is limited to "artifact missing" and "artifact present and valid" for readiness, and "stale project" for the cleanup doctor. Error paths in
dev-dashboard-service.ts(invalid JSON, non-object JSON, unreadableproject.json, non-zero check exit status producingerrors[]) aren't exercised.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/tests/dev-dashboard-vercel-preview-readiness-service.test.ts` around lines 1 - 117, Add negative-path tests for the dev dashboard helpers in readVercelPreviewReadinessArtifact and readVercelCleanupDoctor, since current coverage only checks missing/valid artifacts and one stale-link case. In apps/web/tests/dev-dashboard-vercel-preview-readiness-service.test.ts, cover invalid latest.json JSON, non-object JSON, and a non-zero check status so the resulting errors[] is asserted. Also add a cleanup-doctor case for unreadable or malformed .vercel/project.json so readVercelCleanupDoctor handles it correctly.apps/web/src/app/admin/dev-dashboard/page.tsx (1)
882-885: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winHardcoded admin URL duplicates an existing shared constant.
"https://admin.trr.localhost"is hardcoded here, butapps/web/src/components/admin/AdminGlobalHeader.tsxalready references aPORTLESS_ADMIN_DASHBOARD_URLconstant for exactly this "clean admin URL" concept. If that constant's value ever changes (different TLD, port, etc.), this string will silently drift out of sync.Consider importing and reusing
PORTLESS_ADMIN_DASHBOARD_URLinstead of the literal string.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/app/admin/dev-dashboard/page.tsx` around lines 882 - 885, The “Clean admin URL” text in the dev dashboard is hardcoding the portless admin host instead of reusing the shared constant. Update the dev-dashboard page to import and use PORTLESS_ADMIN_DASHBOARD_URL, matching the pattern already used in AdminGlobalHeader. Replace the literal URL in the Portless Browser Readiness section so the admin URL stays consistent if the shared value changes.apps/web/src/app/api/admin/trr-api/social/profiles/[platform]/[handle]/comments/runs/[runId]/authenticated-followup/route.ts (1)
1-38: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winNear-identical boilerplate across three comments-recovery routes.
This handler,
public-recovery/route.ts, andrepair-auth/route.tsdiffer only in the backend path segment and error strings. Consider extracting a small factory (e.g.createCommentsRunProxyRoute(action, fallbackError)) to avoid triple-maintaining auth, timeout, and error-handling logic.♻️ Example factory sketch
// apps/web/src/lib/server/trr-api/comments-run-proxy.ts export function createCommentsRunProxyRoute(action: string, fallbackError: string) { return async function POST(request: NextRequest, context: RouteContext) { const { platform, handle, runId } = await context.params; try { await requireAdmin(request); const body = await request.text(); const data = await fetchSocialBackendJson( `/profiles/${encodeURIComponent(platform)}/${encodeURIComponent(handle)}/comments/runs/${encodeURIComponent(runId)}/${action}`, { method: "POST", headers: { "Content-Type": "application/json" }, body, fallbackError, retries: 0, timeoutMs: 210_000 }, ); return NextResponse.json(data); } catch (error) { return socialProxyErrorResponse(error, `[api] ${fallbackError}`); } }; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/app/api/admin/trr-api/social/profiles/`[platform]/[handle]/comments/runs/[runId]/authenticated-followup/route.ts around lines 1 - 38, This route duplicates the same comments-run proxy boilerplate found in the other recovery handlers, so extract a shared factory to centralize auth, request body forwarding, timeout, retries, and error handling. Create a helper such as createCommentsRunProxyRoute that takes the action segment and fallback error text, and reuse it from POST instead of keeping separate near-identical implementations in this handler and the other comments-recovery routes. Keep the unique behavior limited to the action path and error strings while preserving requireAdmin, fetchSocialBackendJson, and socialProxyErrorResponse usage.apps/web/src/app/api/admin/trr-api/social-growth/calls/[callId]/route.ts (1)
1-73: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract shared SocialBlade proxy boilerplate.
This handler,
cookies/health/route.ts, andhistory/route.tsall repeat the identical requireAdmin → build backend URL → buildInternalAdminHeaders → timeoutSafeFetch → error-mapping structure, differing only in path, timeout constant, and log string. Sincesocialblade-proxy.tsalready centralizes payload/timeout building, consider extending it with a sharedproxySocialBladeGet(path, { timeoutMs, timeoutName, logLabel })helper to remove ~40 duplicated lines per route and keep future error-handling fixes in one place.♻️ Sketch of a shared helper
// socialblade-proxy.ts export async function proxySocialBladeGet( request: NextRequest, path: string, opts: { timeoutMs: number; timeoutName: string; logLabel: string }, ) { await requireAdmin(request); const backendUrl = getBackendApiUrl(path); if (!backendUrl) { return NextResponse.json({ error: "Backend API not configured (TRR_API_URL)" }, { status: 502 }); } let headers: Headers; try { headers = buildInternalAdminHeaders(); } catch { return NextResponse.json({ error: "Backend auth not configured" }, { status: 502 }); } const upstream = await timeoutSafeFetch(backendUrl, { headers, timeoutMs: opts.timeoutMs, timeoutName: opts.timeoutName }); const data = await upstream.json().catch(() => ({ error: "Invalid response from backend" })); if (!upstream.ok) { return NextResponse.json(buildSocialBladeBackendErrorPayload(data, `Backend returned ${upstream.status}`), { status: upstream.status }); } return NextResponse.json(data); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/app/api/admin/trr-api/social-growth/calls/`[callId]/route.ts around lines 1 - 73, The SocialBlade call status route repeats the same requireAdmin, backend URL, auth header, timeout fetch, and backend error-mapping flow that already exists in the other SocialBlade routes. Move that shared GET-proxy logic into a reusable helper in socialblade-proxy.ts, such as proxySocialBladeGet, and have this handler call it with the route-specific path, timeout settings, and log label. Keep the existing buildSocialBladeBackendErrorPayload and buildSocialBladeTimeoutResponse behavior centralized in the helper so future changes only need to be made once.apps/web/tests/social-growth-cookie-health-route.test.ts (1)
37-115: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd coverage for auth-failure, misconfiguration, and timeout branches.
Current tests only cover the success path and backend-error passthrough. The route also branches on
requireAdminthrowing"forbidden"/"unauthorized",getBackendApiUrlreturningnull(502),buildInternalAdminHeadersthrowing (502), andisTimeoutSafeFetchTimeoutErrorreturning true (504 viabuildSocialBladeTimeoutResponse). None of these are exercised, leaving the newly-added auth/error-mapping logic untested.✅ Example additional cases
it("maps forbidden errors to 403", async () => { requireAdminMock.mockRejectedValue(new Error("forbidden")); const response = await GET(new NextRequest("http://localhost/api/admin/trr-api/social-growth/cookies/health")); expect(response.status).toBe(403); }); it("returns 504 on upstream timeout", async () => { const timeoutError = new Error("timeout"); timeoutSafeFetchMock.mockRejectedValue(timeoutError); isTimeoutSafeFetchTimeoutErrorMock.mockReturnValue(true); const response = await GET(new NextRequest("http://localhost/api/admin/trr-api/social-growth/cookies/health")); expect(response.status).toBe(504); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/tests/social-growth-cookie-health-route.test.ts` around lines 37 - 115, The current social-growth cookie health route tests only cover the happy path and backend error passthrough; add cases in the GET-related test suite to exercise the auth and error-mapping branches. Specifically, in social-growth-cookie-health-route.test.ts, cover requireAdminMock rejecting with "forbidden" and "unauthorized", getBackendApiUrlMock returning null, buildInternalAdminHeadersMock throwing, and timeoutSafeFetchMock rejecting while isTimeoutSafeFetchTimeoutErrorMock returns true. Assert the corresponding response statuses and any timeout response behavior from buildSocialBladeTimeoutResponse so the route’s newly added failure handling stays covered.apps/web/src/app/admin/dev-dashboard/instagram-comments-mockup/page.tsx (2)
202-260: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winOverlapping polling requests aren't guarded; consider the shared live-resource helper instead of ad hoc
setInterval.
refreshDashboardhas no in-flight guard (no ref/AbortController), and theuseEffectfires it immediately then every 5s viawindow.setIntervalregardless of whether the previous call is still pending. If a request takes longer thanREFRESH_INTERVAL_MS, concurrent fetches can resolve out of order and stale data can overwrite fresher state. This PR's stack already introducesapps/web/src/lib/admin/shared-live-resource.tsspecifically to merge shared polling requests and dispose cleanly — reusing it here would avoid re-implementing the same race-prone pattern.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/app/admin/dev-dashboard/instagram-comments-mockup/page.tsx` around lines 202 - 260, `refreshDashboard` is doing ad hoc polling with `window.setInterval` and no in-flight protection, which can create overlapping requests and stale state updates. Refactor the polling logic in `page.tsx` to use the shared live-resource helper from `shared-live-resource.ts` instead of manually scheduling `load`; wire the helper around `refreshDashboard`/`useEffect` so only one request is active at a time and cleanup/disposal happens correctly on unmount or dependency changes.
514-517: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winFull page reload instead of client-side navigation.
window.location.href = canonicalCommentsUrlforces a full browser reload, whereas the "Back to current dashboard" link (Lines 483-489) already usesnext/link'sLinkfor the same kind of navigation. UsingLinkhere keeps the transition consistent and avoids the unnecessary reload cost.Proposed fix
- <Button onClick={() => { window.location.href = canonicalCommentsUrl; }}> - <PlayIcon data-icon="inline-start" /> - Open live dashboard - </Button> + <Button asChild> + <Link href={canonicalCommentsUrl}> + <PlayIcon data-icon="inline-start" /> + Open live dashboard + </Link> + </Button>(Adjust if the local
Buttoncomponent doesn't supportasChild; a plain styledLinkworks equally well.)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/app/admin/dev-dashboard/instagram-comments-mockup/page.tsx` around lines 514 - 517, The Open live dashboard action in the instagram-comments-mockup page is using window.location.href, which causes a full reload instead of client-side navigation. Update the Button interaction in the same area as the existing Back to current dashboard Link so it uses next/link navigation to canonicalCommentsUrl, or render a Link styled like the Button if Button does not support asChild. Keep the change localized to the Open live dashboard control and preserve the current icon/text.apps/web/src/lib/admin/social-account-profile.ts (1)
871-910: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicated public-recovery field set across
retry_progressand top-levelSocialAccountCommentsRunProgress.The six fields
public_comments_recovery_pending_target_count,public_comments_recovery_pending_target_source_ids,public_recovery_bucket,public_comments_approval_required_target_count,public_comments_approval_required_target_source_ids,authenticated_followup_bucketare declared identically insideretry_progress(Lines 877-882) and again at the top level (Lines 904-909), alongsidelargest_remaining_gapsappearing at three separate locations (828, 883). Extracting a shared type would prevent these copies from drifting.♻️ Suggested consolidation
+type SocialAccountCommentsPublicRecoveryState = { + public_comments_recovery_pending_target_count?: number | null; + public_comments_recovery_pending_target_source_ids?: string[]; + public_recovery_bucket?: Record<string, unknown> | null; + public_comments_approval_required_target_count?: number | null; + public_comments_approval_required_target_source_ids?: string[]; + authenticated_followup_bucket?: Record<string, unknown> | null; +};Then spread it with
& SocialAccountCommentsPublicRecoveryStateat both call sites instead of repeating the six fields.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/lib/admin/social-account-profile.ts` around lines 871 - 910, The `SocialAccountCommentsRunProgress` shape repeats the same public-recovery fields in both `retry_progress` and the top-level type, and `largest_remaining_gaps` is also duplicated in multiple places. Extract the shared public-recovery/retry state into a reusable type near `SocialAccountCommentsRunProgress`, then compose it into both `retry_progress` and the top-level declaration so the field list is defined once and can’t drift.apps/web/src/components/admin/instagram/InstagramCatalogBackfillMockupView.tsx (1)
646-663: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoffConsider the shared live-resource polling utility for consistency.
This component implements its own manual polling coordinator (interval + in-flight ref + request-id ref) that mirrors what a shared live-resource polling utility introduced elsewhere in this PR appears designed to centralize. Consolidating would reduce duplicated lifecycle/race-guard logic across admin dashboards.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/components/admin/instagram/InstagramCatalogBackfillMockupView.tsx` around lines 646 - 663, The InstagramCatalogBackfillMockupView polling effect is duplicating the same interval, cancellation, and in-flight/request-id coordination that the shared live-resource polling utility is meant to centralize. Replace the manual useEffect-based polling in this component with the shared polling helper/hook used elsewhere in the PR, wiring it to refreshDashboard and the existing access/loading conditions so the lifecycle and race-guard logic live in one place.apps/web/src/lib/server/admin/portless-status.ts (1)
111-115: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider asserting the new
wordleroute entry in tests.
PORTLESS_STATUS_ROUTESnow includes awordleentry, sobuildPortlessStatusSnapshotwill emit a 4th route object. The existing tests only assert theadmin.trrwildcard case; adding a quick assertion for the wordle entry (present/absent based onportless listoutput) would guard this addition against regressions.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/lib/server/admin/portless-status.ts` around lines 111 - 115, Update the tests around buildPortlessStatusSnapshot to cover the new wordle route in PORTLESS_STATUS_ROUTES, since it now emits a 4th route object. Add an assertion in the existing portless-status test coverage that verifies the wordle entry is included or omitted based on the portless list input, alongside the current admin.trr wildcard checks. Use the buildPortlessStatusSnapshot and PORTLESS_STATUS_ROUTES symbols to locate the relevant test cases.apps/web/src/proxy.ts (2)
1067-1070: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDead
isApiPathcheck insidestrictHostRoutingblock.The new early return at Line 1067-1069 (
if (isApiPath(pathname)) return NextResponse.next();) unconditionally exits for every/api//api/*path before execution ever reaches thestrictHostRoutingblock. This makes theisApiPath(pathname)check at Line 1083-1085 unreachable dead code — it can never evaluate totrueanymore.♻️ Proposed cleanup
if (strictHostRouting && onCanonicalAdminHost && !onAdminUiPath) { - if (isApiPath(pathname)) { - return NextResponse.next(); - } if (!adminOrigin) { return NextResponse.json({ error: "Admin origin is not configured." }, { status: 403 }); } return NextResponse.redirect(new URL("/", adminOrigin), 307); }Also applies to: 1082-1090
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/proxy.ts` around lines 1067 - 1070, The `isApiPath(pathname)` branch inside `strictHostRouting` in `proxy.ts` is now unreachable because the earlier `/api` early return already handles those paths. Remove the dead check from the `strictHostRouting` block and keep the API bypass only in the existing top-level guard so the control flow stays consistent and the duplicate condition is eliminated.
1092-1095: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAvoid hardcoding the Portless admin host here
admin.trr.localhostis already centralized asPORTLESS_ADMIN_ORIGINinadmin-url-defaults.ts; derive the host from that shared constant or add a shared host helper so this check stays in sync with the canonical Portless admin URL.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/proxy.ts` around lines 1092 - 1095, The redirect check in proxy.ts hardcodes the Portless admin host, so update the onCanonicalAdminHost / canonicalAdminHost logic to derive the hostname from the shared PORTLESS_ADMIN_ORIGIN in admin-url-defaults.ts or a shared helper instead of comparing against a literal. Keep the existing NextResponse.redirect behavior, but make the host check reuse the centralized admin URL source so it stays aligned with the canonical Portless admin origin.scripts/test-vercel-guard.sh (1)
107-173: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winNo coverage for the implicit-deploy production bypass.
All production-targeting cases here explicitly include the
deploytoken (deploy --prod,deploy --target=productionvariants). None exercise a bare"$2" --prod(Vercel's implicit-deploy form, nodeploytoken) which — per the companion review comment onscripts/vercel.sh— currently bypassesvercel_deploy_targets_productionentirely. Once that's fixed, add a case like:run_fail_contains \ "implicit deploy production opt-in required" \ "TRR_VERCEL_ALLOW_PROD=1" \ bash -c 'cd "$1" && TRR_VERCEL_PROJECT_GUARD="$3" TRR_VERCEL_GUARD_ONLY=1 "$2" --prod' \ _ "$APP_ROOT" "$VERCEL_SH" "$FAKE_GUARD"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/test-vercel-guard.sh` around lines 107 - 173, The test coverage in test-vercel-guard.sh misses Vercel’s implicit production deploy form, where the command is invoked as "$2" --prod without an explicit deploy token. Add a guard assertion in the existing production-targeting section using run_fail_contains with the same TRR_VERCEL_PROJECT_GUARD and TRR_VERCEL_GUARD_ONLY setup, and point it at the "$2" --prod path so it verifies the production opt-in requirement is enforced for implicit deploys too.scripts/vercel.sh (1)
269-297: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winScope-fallback wrapper merges stdout/stderr on the success path, losing stream separation the caller relies on.
On a successful primary attempt,
run_vercel_cli_with_scope_fallbackcaptures the CLI's combinedstdout+stderr(via2>&1) into$outputand re-emits it all throughprintfto stdout only. Callers inrun_preview_ready(e.g. Line 496, 504, 512) redirect this wrapper's stdout/stderr into separate files ($web_stdout/$web_stderr, etc.), which the embedded Python script later parses for JSON (parse_json_from_cli_output). Any warnings/progress text the CLI would normally send to stderr now lands in thestdoutfile instead, adding noise to the JSON-boundary heuristic (raw.find("{")/raw.rfind("}")) and leaving the corresponding stderr file empty. Only the fallback (team-id) branch preserves the original stream separation.Also applies to: 494-515
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/vercel.sh` around lines 269 - 297, The scope-fallback helper in run_vercel_cli_with_scope_fallback is collapsing stdout and stderr on the primary success path, which breaks callers that depend on separate streams. Update the wrapper so run_vercel_cli output is preserved per stream: keep stdout and stderr distinct when the primary scope attempt succeeds, and only use combined capture for detecting the specific scope-missing error before retrying. Make sure the retry/fallback path still works, and verify the run_preview_ready callers and parse_json_from_cli_output continue receiving clean stdout with warnings/progress routed to stderr.apps/web/scripts/perf-admin-api.mjs (1)
121-129: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUnpinned
autocannonversion vianpx --yesundermines run-to-run comparability.Each invocation resolves whatever
autocannonversion npx fetches at runtime. For a script meant to track latency regressions over time, an unpinned dependency version can introduce noise unrelated to the code under test. Consider pinning, e.g.npx --yes autocannon@<version>, mirroring howrun_vercel_clipins the Vercel CLI version elsewhere in this PR.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/scripts/perf-admin-api.mjs` around lines 121 - 129, The autocannon invocation used by the perf admin script is not version-pinned, so each run may use a different runtime release and skew latency comparisons. Update the spawnSync call that launches npx/autocannon in the perf admin flow to use a fixed autocannon version, matching the pinned-tool pattern used by run_vercel_cli elsewhere in the PR, so repeated benchmark runs remain comparable.apps/web/src/app/api/admin/trr-api/social/profiles/[platform]/[handle]/catalog/runs/[runId]/progress/route.ts (1)
102-129: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueRename
fast_progress_timeoutThe fast and normal paths shareSOCIAL_PROXY_PROGRESS_TIMEOUT_MS, so this reason no longer reflects a special fast timeout.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/app/api/admin/trr-api/social/profiles/`[platform]/[handle]/catalog/runs/[runId]/progress/route.ts around lines 102 - 129, The degraded progress reason used in the route handler is misleading because the fast and normal paths both use SOCIAL_PROXY_PROGRESS_TIMEOUT_MS, so fast_progress_timeout no longer represents a distinct timeout case. Update the progress route logic around isFastPoll, buildDegradedProgressPayload, and markProgressDegraded to use a more accurate reason string for this fallback, and ensure any associated consumers or tests are updated to match the new, generic timeout/degraded naming.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: ca60f9ba-ebfb-4333-b77b-6b2c0decec24
⛔ Files ignored due to path filters (2)
apps/web/src/lib/admin/api-references/generated/inventory.tsis excluded by!**/generated/**pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml,!pnpm-lock.yaml
📒 Files selected for processing (106)
apps/web/README.mdapps/web/next.config.tsapps/web/package.jsonapps/web/playwright.config.tsapps/web/scripts/perf-admin-api.mjsapps/web/src/app/admin/dev-dashboard/_components/DevDashboardShell.tsxapps/web/src/app/admin/dev-dashboard/instagram-catalog-backfill-mockup/page.tsxapps/web/src/app/admin/dev-dashboard/instagram-comments-mockup/page.tsxapps/web/src/app/admin/dev-dashboard/page.tsxapps/web/src/app/admin/page.tsxapps/web/src/app/admin/social/page.tsxapps/web/src/app/admin/social/reddit/[communitySlug]/[showSlug]/s[seasonNumber]/[windowKey]/[detailSlug]/page.tsxapps/web/src/app/admin/social/reddit/[communitySlug]/[showSlug]/s[seasonNumber]/[windowKey]/page.tsxapps/web/src/app/admin/social/reddit/[communitySlug]/[showSlug]/s[seasonNumber]/[windowKey]/post/[postId]/page.tsxapps/web/src/app/admin/social/reddit/[communitySlug]/[showSlug]/s[seasonNumber]/page.tsxapps/web/src/app/admin/trr-shows/[showId]/page.tsxapps/web/src/app/admin/trr-shows/page.tsxapps/web/src/app/api/admin/social/media-queue/snapshots/route.tsapps/web/src/app/api/admin/trr-api/social-growth/calls/[callId]/route.tsapps/web/src/app/api/admin/trr-api/social-growth/cookies/health/route.tsapps/web/src/app/api/admin/trr-api/social-growth/history/route.tsapps/web/src/app/api/admin/trr-api/social/ingest/media-mirror/drain-account/route.tsapps/web/src/app/api/admin/trr-api/social/ingest/media-mirror/recover-stale/route.tsapps/web/src/app/api/admin/trr-api/social/profiles/[platform]/[handle]/catalog/runs/[runId]/progress/route.tsapps/web/src/app/api/admin/trr-api/social/profiles/[platform]/[handle]/catalog/runs/recent/route.tsapps/web/src/app/api/admin/trr-api/social/profiles/[platform]/[handle]/comments/runs/[runId]/authenticated-followup/route.tsapps/web/src/app/api/admin/trr-api/social/profiles/[platform]/[handle]/comments/runs/[runId]/public-recovery/route.tsapps/web/src/app/api/admin/trr-api/social/profiles/[platform]/[handle]/comments/runs/[runId]/repair-auth/route.tsapps/web/src/app/api/admin/trr-api/social/profiles/[platform]/[handle]/completion-summary/route.tsapps/web/src/app/api/admin/trr-api/social/profiles/[platform]/[handle]/live-profile-total/route.tsapps/web/src/app/api/admin/trr-api/social/profiles/[platform]/[handle]/snapshot/route.tsapps/web/src/app/auth/finish/page.tsxapps/web/src/app/layout.tsxapps/web/src/app/social/[platform]/[handle]/catalog/alt-1/page.tsxapps/web/src/components/admin/AdminGlobalHeader.tsxapps/web/src/components/admin/SocialAccountProfileHashtagTimelineChart.tsxapps/web/src/components/admin/SocialAccountProfilePage.tsxapps/web/src/components/admin/cast-content-section.tsxapps/web/src/components/admin/cast-socialblade-comparison.tsxapps/web/src/components/admin/design-system/BrandFontMatchesPanel.tsxapps/web/src/components/admin/instagram/InstagramCatalogBackfillMockupView.tsxapps/web/src/components/admin/instagram/InstagramCommentsPanel.tsxapps/web/src/components/admin/season-social-analytics-section.tsxapps/web/src/components/admin/social-growth-section.tsxapps/web/src/components/admin/social-week/WeekDetailPageView.tsxapps/web/src/components/ui/alert.tsxapps/web/src/components/ui/badge.tsxapps/web/src/components/ui/card.tsxapps/web/src/components/ui/input.tsxapps/web/src/components/ui/separator.tsxapps/web/src/lib/admin/admin-url-defaults.tsapps/web/src/lib/admin/api-references/generator.tsapps/web/src/lib/admin/shared-live-resource.tsapps/web/src/lib/admin/social-account-profile.tsapps/web/src/lib/admin/social-timezone.tsapps/web/src/lib/data/shows.tsapps/web/src/lib/fonts/brand-fonts/scoring-profiles.tsapps/web/src/lib/fonts/brand-fonts/scoring.tsapps/web/src/lib/server/admin/dev-dashboard-service.tsapps/web/src/lib/server/admin/portless-status.tsapps/web/src/lib/server/postgres.tsapps/web/src/lib/trr-app-flags.tsapps/web/src/proxy.tsapps/web/tests/admin-api-references-generator.test.tsapps/web/tests/admin-host-middleware.test.tsapps/web/tests/admin-portless-status.test.tsapps/web/tests/admin-social-page-auth-bypass.test.tsxapps/web/tests/brand-font-matching-scoring.test.tsapps/web/tests/cast-socialblade-snapshot-wiring.test.tsapps/web/tests/covered-shows-page-no-fanout-wiring.test.tsapps/web/tests/dev-dashboard-vercel-preview-readiness-service.test.tsapps/web/tests/e2e/admin-cast-tabs-smoke.spec.tsapps/web/tests/e2e/admin-fixtures.tsapps/web/tests/finish.flow.test.tsxapps/web/tests/instagram-catalog-backfill-mockup-page.test.tsxapps/web/tests/postgres-connection-string-resolution.test.tsapps/web/tests/prefixed-pathname.test.tsapps/web/tests/public-route-boundary.test.tsapps/web/tests/shared-env-contract.test.tsapps/web/tests/shared-live-resource-polling.test.tsxapps/web/tests/show-admin-routes.test.tsapps/web/tests/show-refresh-health-center-wiring.test.tsapps/web/tests/shows-route-config.test.tsapps/web/tests/social-account-catalog-alt-page.test.tsapps/web/tests/social-account-catalog-recent-runs-route.test.tsapps/web/tests/social-account-catalog-run-progress-route.test.tsapps/web/tests/social-account-hashtag-timeline.runtime.test.tsxapps/web/tests/social-account-live-profile-total-route.test.tsapps/web/tests/social-account-profile-backfill-command.test.tsapps/web/tests/social-account-profile-catalog-diagnostics.test.tsapps/web/tests/social-account-profile-completion-summary-route.test.tsapps/web/tests/social-account-profile-page.hashtag-preview.test.tsapps/web/tests/social-account-profile-page.runtime.test.tsxapps/web/tests/social-account-profile-route.test.tsapps/web/tests/social-account-profile-snapshot-route.test.tsapps/web/tests/social-growth-cookie-health-route.test.tsapps/web/tests/social-ingest-media-mirror-drain-account-route.test.tsapps/web/tests/social-ingest-media-mirror-recover-route.test.tsapps/web/tests/social-media-queue-snapshots-route.test.tsapps/web/tests/social-timezone.test.tsapps/web/tests/trr-app-flags.test.tspackage.jsonpnpm-workspace.yamlscripts/portless-banner.mjsscripts/test-vercel-guard.shscripts/vercel.sh
| const ready = | ||
| snapshot?.status === "ok" && | ||
| serviceReady && | ||
| appRouteReady && | ||
| adminRouteReady && | ||
| apiRouteReady && | ||
| snapshot.uses_static_aliases !== true; | ||
| const blockers = [ | ||
| service?.installed === false ? "Portless service is not installed" : null, | ||
| service?.proxy_on_443 === false ? "Portless secure proxy is not responding" : null, | ||
| service?.wildcard === false ? "Wildcard routing is off" : null, | ||
| !appRouteReady ? "TRR app route is missing" : null, | ||
| !apiRouteReady ? "TRR API route is missing" : null, | ||
| snapshot?.uses_static_aliases ? "Static aliases are still active" : null, | ||
| snapshot?.status === "unavailable" ? snapshot.raw_error || "Portless route list is unavailable" : null, | ||
| ].filter((item): item is string => Boolean(item)); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
adminRouteReady gates readiness but has no matching blocker message.
ready requires adminRouteReady to be true (line 866), but the blockers array never adds a message when adminRouteReady is false. If every other check passes and only the admin route is missing, ready becomes false ("Blocked" badge) while blockers.length === 0, so the "What is blocking Browser" explanation section (which is gated on blockers.length > 0) never renders — leaving the admin with a "Blocked" badge and no explanation.
🐛 Proposed fix
const blockers = [
service?.installed === false ? "Portless service is not installed" : null,
service?.proxy_on_443 === false ? "Portless secure proxy is not responding" : null,
service?.wildcard === false ? "Wildcard routing is off" : null,
!appRouteReady ? "TRR app route is missing" : null,
+ !adminRouteReady ? "Admin route is missing" : null,
!apiRouteReady ? "TRR API route is missing" : null,
snapshot?.uses_static_aliases ? "Static aliases are still active" : null,
snapshot?.status === "unavailable" ? snapshot.raw_error || "Portless route list is unavailable" : null,
].filter((item): item is string => Boolean(item));📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const ready = | |
| snapshot?.status === "ok" && | |
| serviceReady && | |
| appRouteReady && | |
| adminRouteReady && | |
| apiRouteReady && | |
| snapshot.uses_static_aliases !== true; | |
| const blockers = [ | |
| service?.installed === false ? "Portless service is not installed" : null, | |
| service?.proxy_on_443 === false ? "Portless secure proxy is not responding" : null, | |
| service?.wildcard === false ? "Wildcard routing is off" : null, | |
| !appRouteReady ? "TRR app route is missing" : null, | |
| !apiRouteReady ? "TRR API route is missing" : null, | |
| snapshot?.uses_static_aliases ? "Static aliases are still active" : null, | |
| snapshot?.status === "unavailable" ? snapshot.raw_error || "Portless route list is unavailable" : null, | |
| ].filter((item): item is string => Boolean(item)); | |
| const ready = | |
| snapshot?.status === "ok" && | |
| serviceReady && | |
| appRouteReady && | |
| adminRouteReady && | |
| apiRouteReady && | |
| snapshot.uses_static_aliases !== true; | |
| const blockers = [ | |
| service?.installed === false ? "Portless service is not installed" : null, | |
| service?.proxy_on_443 === false ? "Portless secure proxy is not responding" : null, | |
| service?.wildcard === false ? "Wildcard routing is off" : null, | |
| !appRouteReady ? "TRR app route is missing" : null, | |
| !adminRouteReady ? "Admin route is missing" : null, | |
| !apiRouteReady ? "TRR API route is missing" : null, | |
| snapshot?.uses_static_aliases ? "Static aliases are still active" : null, | |
| snapshot?.status === "unavailable" ? snapshot.raw_error || "Portless route list is unavailable" : null, | |
| ].filter((item): item is string => Boolean(item)); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/app/admin/dev-dashboard/page.tsx` around lines 861 - 876,
`ready` depends on `adminRouteReady`, but `blockers` in
`admin/dashboard/page.tsx` never reports that failure, which can leave the UI
blocked with no explanation. Update the `blockers` array next to `ready` so it
includes a message when `adminRouteReady` is false, matching the existing
`appRouteReady` and `apiRouteReady` checks, and keep the message consistent with
the route naming used in this section.
| disabled={!runIdValid || recovering} | ||
| className="min-h-10" | ||
| > | ||
| <RefreshCw aria-hidden="true" className={recovering ? "animate-spin" : ""} /> | ||
| Recover stale | ||
| </Button> | ||
| <input | ||
| type="text" | ||
| value={drainAccountHandle} | ||
| onChange={(event) => onDrainAccountHandleChange(event.target.value)} | ||
| placeholder="Account" | ||
| aria-label="Drain media account" | ||
| className="min-h-10 rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900 shadow-sm outline-none transition focus:border-cyan-500 focus:ring-2 focus:ring-cyan-500/20" | ||
| /> | ||
| <Button | ||
| type="button" | ||
| onClick={onDrain} | ||
| disabled={!runIdValid || draining} | ||
| className="min-h-10" | ||
| variant="secondary" | ||
| > | ||
| <RefreshCw aria-hidden="true" className={draining ? "animate-spin" : ""} /> | ||
| Drain Bravo media | ||
| </Button> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Recover/Drain buttons silently no-op when adminSocialScraperTriggersEnabled is off.
MediaQueueDrilldownPanel never receives adminSocialScraperTriggersEnabled as a prop, so its disabled conditions at Lines 1188 and 1205 only check runIdValid/recovering/draining. Meanwhile recoverStaleMediaQueueJobs (Line 2974) and drainBravoMediaQueueJobs (Line 3057) both return immediately when !adminSocialScraperTriggersEnabled, without setting any error/message state. So whenever adminSocialIngestionUi is on but adminSocialScraperTriggers is off, an admin can click "Recover stale" / "Drain Bravo media" with a valid run ID and see nothing happen — no spinner, no error, no confirmation. This is especially risky during an incident where an operator might assume the action succeeded (or is pending) when it never fired. No test in admin-social-page-auth-bypass.test.tsx covers this flag combination.
🐛 Proposed fix
const recoverStaleMediaQueueJobs = async () => {
- if (!user || !adminSocialScraperTriggersEnabled) return;
+ if (!user) return;
+ if (!adminSocialScraperTriggersEnabled) {
+ setMediaRecoveryError("Scraper triggers are disabled in this environment.");
+ return;
+ }
const runId = mediaRecoveryRunId.trim(); const drainBravoMediaQueueJobs = async () => {
- if (!user || !adminSocialScraperTriggersEnabled) return;
+ if (!user) return;
+ if (!adminSocialScraperTriggersEnabled) {
+ setMediaDrainError("Scraper triggers are disabled in this environment.");
+ return;
+ }
const runId = mediaRecoveryRunId.trim();And disable the buttons proactively by threading the flag into the panel:
const MediaQueueDrilldownPanel = ({
summary,
runId,
stage,
+ actionsEnabled,
...
}: {
...
+ actionsEnabled: boolean;
...
}) => {
...
<Button
type="button"
onClick={onRecover}
- disabled={!runIdValid || recovering}
+ disabled={!runIdValid || recovering || !actionsEnabled}
...
<Button
type="button"
onClick={onDrain}
- disabled={!runIdValid || draining}
+ disabled={!runIdValid || draining || !actionsEnabled}(pass actionsEnabled={adminSocialScraperTriggersEnabled} at the call site around Line 3612).
Also applies to: 2973-3155
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/app/admin/social/page.tsx` around lines 1188 - 1211,
Recover/Drain currently no-op when adminSocialScraperTriggersEnabled is off
because MediaQueueDrilldownPanel doesn’t receive that flag, so its buttons stay
enabled even though recoverStaleMediaQueueJobs and drainBravoMediaQueueJobs
return immediately. Thread the flag into MediaQueueDrilldownPanel as
actionsEnabled from the call site, and use it in the disabled logic for the
“Recover stale” and “Drain Bravo media” buttons so they’re proactively disabled
whenever the actions are unavailable.
| const appFlags = getTrrAppFlags(); | ||
| const adminSocialIngestionUiEnabled = appFlags.adminSocialIngestionUi; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd trr-app-flags.ts
cat -n apps/web/src/lib/trr-app-flags.ts 2>/dev/null | sed -n '1,140p'Repository: therealityreport/trr-app
Length of output: 3481
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the page and flag helper usage
ast-grep outline apps/web/src/app/admin/social/page.tsx --view expanded || true
printf '\n--- page snippet ---\n'
sed -n '1,220p' apps/web/src/app/admin/social/page.tsx
printf '\n--- flag helper usages ---\n'
rg -n "getTrrAppFlags|getTrrAppFlagValue|TRR_APP_FLAGS|NEXT_PUBLIC_TRR_FLAG_" apps/web/src -SRepository: therealityreport/trr-app
Length of output: 12855
🌐 Web query:
Next.js environment variables dynamic access process.env bracket notation client bundle static analyzable NEXT_PUBLIC
💡 Result:
In Next.js, environment variables prefixed with NEXT_PUBLIC_ are designed to be accessible in client-side code [1][2]. To ensure these values are correctly bundled, Next.js utilizes a static analysis process during the build phase (e.g., via Webpack's DefinePlugin or Turbopack) that scans for literal references to process.env.NEXT_PUBLIC_... and replaces them with their corresponding hard-coded values [3][4][1]. Because of this static replacement mechanism, the following limitations apply to client-side code: 1. Bracket Notation and Dynamic Access: Using dynamic keys or bracket notation (e.g., process.env[myVariable]) does not work for NEXT_PUBLIC_ variables [3][1]. The build process cannot determine the value of the key at build time, so it cannot perform the required static replacement, resulting in undefined [3][1]. 2. Destructuring: Object destructuring (e.g., const { NEXT_PUBLIC_KEY } = process.env) is generally unsupported for these variables in client bundles because the bundler cannot match the specific property access pattern required for replacement [3][1][5]. 3. Build-Time Inlining: Since the replacement happens at build time, these values are "baked" into your client bundle [1][2]. Changing an environment variable on your server or hosting platform (like Vercel) after a build will not update the client-side value; you must trigger a new build and deployment for the change to take effect [1][2]. To work around these limitations if dynamic access is strictly required, you can manually create an object in your code that maps the necessary environment variables and use that object instead of relying directly on process.env [6]. Additionally, keep in mind that non-prefixed variables are strictly server-side only and are never included in the client bundle, which is a security feature to prevent sensitive secrets from being exposed to the browser [1][2]. Top results: [3], [4], [1], [2]
Citations:
- 1: https://env.dev/guides/nextjs-env-variables
- 2: https://envtools.dev/guides/next-public-env-variables
- 3: Environment variables hidden when using dinamic retrieval vercel/next.js#35664
- 4: https://stackoverflow.com/questions/64152943/cant-access-process-env-values-using-dynamic-keys
- 5: https://nextjs.org/docs/app/api-reference/config/next-config-js/env
- 6: https://stackoverflow.com/questions/68314905/dynamic-access-of-environment-variables-in-nextjs-not-working
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf -- '--- next config files ---\n'
fd -H 'next.config.*' apps/web .
printf -- '\n--- env injection references ---\n'
rg -n "NEXT_PUBLIC_TRR_FLAG_|env:\\s*\\{|publicRuntimeConfig|runtimeConfig|process\\.env\\[" apps/web -S
printf -- '\n--- next.config excerpt ---\n'
for f in $(fd -H 'next.config.*' apps/web .); do
echo "FILE: $f"
sed -n '1,220p' "$f"
echo
doneRepository: therealityreport/trr-app
Length of output: 26131
Resolve getTrrAppFlags() for client bundles
apps/web/src/lib/trr-app-flags.ts looks up process.env dynamically (env[definition.envVar]), so NEXT_PUBLIC_* values won’t be inlined in this use client page. That can leave adminSocialIngestionUi false in the browser and create a server/client mismatch. Switch to statically referenced process.env.NEXT_PUBLIC_* access or pass the resolved flags from the server.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/app/admin/social/page.tsx` around lines 2871 - 2872, The client
page is calling getTrrAppFlags(), but that helper resolves env vars dynamically
via env[definition.envVar], which won’t inline NEXT_PUBLIC_* values in a use
client bundle. Update the admin social page to avoid dynamic flag resolution in
the browser by either reading statically referenced process.env.NEXT_PUBLIC_*
values or passing the resolved flags down from the server, so
adminSocialIngestionUi stays consistent between server and client.
| redirect( | ||
| `/${encodeURIComponent(showSlug)}/social/reddit/${encodeURIComponent(communitySlug)}/${encodeURIComponent( | ||
| seasonNumber, | ||
| )}/${encodeURIComponent(windowKey)}/${encodeURIComponent(detailSlug)}`, | ||
| ); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== file list ==\n'
git ls-files 'apps/web/src/app/admin/social/reddit/**' | sed -n '1,120p'
printf '\n== target file lines ==\n'
wc -l apps/web/src/app/admin/social/reddit/[communitySlug]/[showSlug]/s[seasonNumber]/[windowKey]/[detailSlug]/page.tsx
cat -n apps/web/src/app/admin/social/reddit/[communitySlug]/[showSlug]/s[seasonNumber]/[windowKey]/[detailSlug]/page.tsx | sed -n '1,120p'
printf '\n== search for social reddit route patterns ==\n'
rg -n --hidden --glob '!**/node_modules/**' 's\[seasonNumber\]|seasonNumber' apps/web/src/app/admin/social/reddit -SRepository: therealityreport/trr-app
Length of output: 4264
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== route tree ==\n'
find apps/web/src/app/admin/social/reddit -maxdepth 6 -type f | sort | sed -n '1,200p'
printf '\n== relevant page files ==\n'
fd -a 'page.tsx' apps/web/src/app/admin/social/reddit | sed -n '1,200p'Repository: therealityreport/trr-app
Length of output: 2611
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== no-season detail page ==\n'
cat -n apps/web/src/app/admin/social/reddit/[communitySlug]/[showSlug]/[windowKey]/[detailSlug]/page.tsx | sed -n '1,160p'
printf '\n== seasonal index page ==\n'
cat -n apps/web/src/app/admin/social/reddit/[communitySlug]/[showSlug]/s[seasonNumber]/[windowKey]/page.tsx | sed -n '1,160p'
printf '\n== non-season index page ==\n'
cat -n apps/web/src/app/admin/social/reddit/[communitySlug]/[showSlug]/[windowKey]/page.tsx | sed -n '1,160p'
printf '\n== seasonal detail page ==\n'
cat -n apps/web/src/app/admin/social/reddit/[communitySlug]/[showSlug]/s[seasonNumber]/[windowKey]/[detailSlug]/page.tsx | sed -n '1,160p'Repository: therealityreport/trr-app
Length of output: 1950
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== public social/reddit route tree ==\n'
find apps/web/src/app -path '*/social/reddit/*' -type f | sort | sed -n '1,240p'
printf '\n== route files with season folders ==\n'
rg -n --hidden --glob '!**/node_modules/**' 's\[seasonNumber\]|\[seasonNumber\]' apps/web/src/app -SRepository: therealityreport/trr-app
Length of output: 5988
Add the s prefix to the season segment
This redirect should build .../s${seasonNumber}/...; the current path uses just the raw season number and won’t match the seasonal route.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@apps/web/src/app/admin/social/reddit/`[communitySlug]/[showSlug]/s[seasonNumber]/[windowKey]/[detailSlug]/page.tsx
around lines 17 - 21, The redirect in the page.tsx handler is missing the
seasonal route prefix, so it builds the wrong path. Update the redirect in the
page component to include the `s` prefix before `seasonNumber` when constructing
the social/reddit URL, keeping the other encoded segments unchanged.
| redirect( | ||
| `/${encodeURIComponent(showSlug)}/social/reddit/${encodeURIComponent(communitySlug)}/${encodeURIComponent( | ||
| seasonNumber, | ||
| )}/${encodeURIComponent(windowKey)}/${encodeURIComponent(postId)}`, | ||
| ); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the route file and related sibling pages.
git ls-files 'apps/web/src/app/admin/social/reddit/**' | sort
printf '\n--- route references for season/window/post ---\n'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/.next/**' \
's\$\{encodeURIComponent\(seasonNumber\)\}|seasonNumber|windowKey|postId|communitySlug|showSlug|social/reddit' \
apps/web/src/app/admin/social/reddit
printf '\n--- files named page.tsx under this route tree ---\n'
fd -a 'page\.tsx$' apps/web/src/app/admin/social/redditRepository: therealityreport/trr-app
Length of output: 7834
Add the s prefix in this redirect. The target path should match the s[seasonNumber] route; without s${encodeURIComponent(seasonNumber)} this redirect points to a non-existent URL.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@apps/web/src/app/admin/social/reddit/`[communitySlug]/[showSlug]/s[seasonNumber]/[windowKey]/post/[postId]/page.tsx
around lines 15 - 19, The redirect in the post page route is missing the
required season segment prefix, so it does not match the s[seasonNumber] route.
Update the redirect URL construction in the post page handler to include the
literal s prefix before the encoded season number, alongside the existing
showSlug, communitySlug, windowKey, and postId segments.
| </div> | ||
| <span className="rounded-md bg-white px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-zinc-600 ring-1 ring-inset ring-zinc-200"> | ||
| {profileLabel(breakdown.profile)} | ||
| {breakdown.profile} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Raw profile enum shown instead of human-readable label.
With profileLabel removed, this now renders breakdown.profile directly, so admins see raw values like explicit-mapping-visual or metadata-only instead of a formatted label. Consider keeping a small display-label mapping (can live alongside getScoreBreakdownMaxima in scoring-profiles.ts for reuse) rather than exposing the internal enum verbatim.
💡 Suggested fix
- <span className="rounded-md bg-white px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-zinc-600 ring-1 ring-inset ring-zinc-200">
- {breakdown.profile}
- </span>
+ <span className="rounded-md bg-white px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-zinc-600 ring-1 ring-inset ring-zinc-200">
+ {getScoreBreakdownProfileLabel(breakdown.profile)}
+ </span>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/components/admin/design-system/BrandFontMatchesPanel.tsx` at
line 193, The profile text in BrandFontMatchesPanel is now rendering the raw
breakdown.profile enum instead of a user-friendly label. Update the display
logic in BrandFontMatchesPanel to use a human-readable label again, and add or
reuse a small profile-to-label mapping in scoring-profiles.ts alongside
getScoreBreakdownMaxima so the internal enum values like explicit-mapping-visual
or metadata-only are not shown directly to admins.
| const readFiniteNumber = (value: unknown): number | null => { | ||
| const numeric = Number(value); | ||
| return Number.isFinite(numeric) ? numeric : null; | ||
| }; | ||
|
|
||
| const readRecord = (value: unknown): Record<string, unknown> | null => { | ||
| return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : null; | ||
| }; | ||
|
|
||
| const readString = (value: unknown): string | null => { | ||
| return typeof value === "string" && value.trim() ? value.trim() : null; | ||
| }; | ||
|
|
||
| const readRecordNumber = (record: Record<string, unknown> | null | undefined, keys: string[]): number | null => { | ||
| if (!record) return null; | ||
| for (const key of keys) { | ||
| const value = readFiniteNumber(record[key]); | ||
| if (value !== null) return value; | ||
| } | ||
| return null; | ||
| }; | ||
|
|
||
| const firstFiniteNumber = (...values: unknown[]): number | null => { | ||
| for (const value of values) { | ||
| const numeric = readFiniteNumber(value); | ||
| if (numeric !== null) return numeric; | ||
| } | ||
| return null; | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
readFiniteNumber mishandles explicit null, breaking fallback chains.
Number(null) evaluates to 0 in JavaScript, so readFiniteNumber(null) returns 0 instead of null. Since many payload fields are typed number | null (e.g. post_progress.completed_posts, shard_index), this makes firstFiniteNumber treat "not yet computed" as "0" and stop looking at later fallback candidates (line 670-674), and makes shard rows with shard_index: null render as "Shard 1" instead of falling back to the row-index label (lines 428-436).
🐛 Proposed fix
const readFiniteNumber = (value: unknown): number | null => {
- const numeric = Number(value);
- return Number.isFinite(numeric) ? numeric : null;
+ if (value === null || value === undefined) return null;
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const readFiniteNumber = (value: unknown): number | null => { | |
| const numeric = Number(value); | |
| return Number.isFinite(numeric) ? numeric : null; | |
| }; | |
| const readRecord = (value: unknown): Record<string, unknown> | null => { | |
| return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : null; | |
| }; | |
| const readString = (value: unknown): string | null => { | |
| return typeof value === "string" && value.trim() ? value.trim() : null; | |
| }; | |
| const readRecordNumber = (record: Record<string, unknown> | null | undefined, keys: string[]): number | null => { | |
| if (!record) return null; | |
| for (const key of keys) { | |
| const value = readFiniteNumber(record[key]); | |
| if (value !== null) return value; | |
| } | |
| return null; | |
| }; | |
| const firstFiniteNumber = (...values: unknown[]): number | null => { | |
| for (const value of values) { | |
| const numeric = readFiniteNumber(value); | |
| if (numeric !== null) return numeric; | |
| } | |
| return null; | |
| }; | |
| const readFiniteNumber = (value: unknown): number | null => { | |
| if (value === null || value === undefined) return null; | |
| const numeric = Number(value); | |
| return Number.isFinite(numeric) ? numeric : null; | |
| }; | |
| const readRecord = (value: unknown): Record<string, unknown> | null => { | |
| return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : null; | |
| }; | |
| const readString = (value: unknown): string | null => { | |
| return typeof value === "string" && value.trim() ? value.trim() : null; | |
| }; | |
| const readRecordNumber = (record: Record<string, unknown> | null | undefined, keys: string[]): number | null => { | |
| if (!record) return null; | |
| for (const key of keys) { | |
| const value = readFiniteNumber(record[key]); | |
| if (value !== null) return value; | |
| } | |
| return null; | |
| }; | |
| const firstFiniteNumber = (...values: unknown[]): number | null => { | |
| for (const value of values) { | |
| const numeric = readFiniteNumber(value); | |
| if (numeric !== null) return numeric; | |
| } | |
| return null; | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@apps/web/src/components/admin/instagram/InstagramCatalogBackfillMockupView.tsx`
around lines 133 - 161, `readFiniteNumber` currently treats explicit null as 0,
which breaks fallback logic in `firstFiniteNumber` and the shard labeling path.
Update `readFiniteNumber` in `InstagramCatalogBackfillMockupView` to return null
for null/undefined inputs before calling Number(), while still accepting valid
numeric strings and finite numbers. This will let `firstFiniteNumber` continue
past absent values and allow the shard row logic to fall back correctly when
`shard_index` is null.
| function stripPathQueryTemplate(raw: string): string { | ||
| let templateDepth = 0; | ||
| for (let index = 0; index < raw.length; index += 1) { | ||
| const current = raw[index]; | ||
| const next = raw[index + 1]; | ||
| if (current === "$" && next === "{") { | ||
| if (templateDepth === 0) { | ||
| const expression = raw.slice(index + 2); | ||
| if ( | ||
| /^\s*(?:query|search|searchParams|queryParams|urlSearchParams)\b/i.test(expression) || | ||
| /^\s*[^}]*\?\s*`?\?/.test(expression) || | ||
| /^\s*[^}]*:\s*`?\s*`/.test(expression) | ||
| ) { | ||
| return raw.slice(0, index); | ||
| } | ||
| } | ||
| templateDepth += 1; | ||
| index += 1; | ||
| continue; | ||
| } | ||
| if (current === "}" && templateDepth > 0) { | ||
| templateDepth -= 1; | ||
| continue; | ||
| } | ||
| if (templateDepth === 0 && (current === "?" || current === "#")) { | ||
| return raw.slice(0, index); | ||
| } | ||
| } | ||
| return raw; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for existing UI request call sites using ?? or ?. inside template path segments,
# which would currently be mis-truncated by stripPathQueryTemplate.
rg -nP '\$\{[^}]*\?\?[^}]*\}' apps/web/src --type=ts --type=tsx -g '!**/tests/**'
rg -nP '\$\{[^}]*\?\.[^}]*\?\?[^}]*\}' apps/web/src --type=ts --type=tsx -g '!**/tests/**'Repository: therealityreport/trr-app
Length of output: 218
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the function and nearby normalization logic.
file='apps/web/src/lib/admin/api-references/generator.ts'
wc -l "$file"
sed -n '150,250p' "$file"
# Find call sites / related helpers.
rg -n "stripPathQueryTemplate|normalizeStringPathPattern|pathPattern" apps/web/src/lib/admin/api-references -g '*.ts'
# Search for template literals in request paths that use nullish coalescing / optional chaining.
rg -n '\$\{[^}]*\?\?[^}]*\}' apps/web/src --glob '*.ts' --glob '*.tsx' -g '!**/tests/**'
rg -n '\$\{[^}]*\?\.[^}]*\?\?[^}]*\}' apps/web/src --glob '*.ts' --glob '*.tsx' -g '!**/tests/**'Repository: therealityreport/trr-app
Length of output: 50381
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the file structure first, then inspect only the relevant ranges.
file='apps/web/src/lib/admin/api-references/generator.ts'
ast-grep outline "$file" --view expandedRepository: therealityreport/trr-app
Length of output: 5209
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import re
patterns = [
(r'^\s*[^}]*\?\s*`?\?', "optional-backtick regex"),
(r'^\s*[^}]*\?\s*`\s*\?', "required-backtick regex"),
]
samples = [
"id ?? 'default'",
"obj?.id ?? 'x'",
"cond ? `?${foo}` : bar",
"cond ? `?${foo}?` : bar",
"cond ? ?",
"query ? `?${foo}` : bar",
]
for sample in samples:
print(f"SAMPLE: {sample!r}")
for pat, name in patterns:
print(name, bool(re.search(pat, sample)))
print()
PYRepository: therealityreport/trr-app
Length of output: 690
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read the exact function and the surrounding normalize helper.
nl -ba apps/web/src/lib/admin/api-references/generator.ts | sed -n '170,235p'Repository: therealityreport/trr-app
Length of output: 202
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for any code that constructs path-like templates with nullish coalescing
# so we can judge whether the alleged false positive is reachable.
rg -n '\$\{[^}]*\?\?[^}]*\}' apps/web --glob '*.ts' --glob '*.tsx' --glob '*.js' --glob '*.jsx' -g '!**/tests/**'Repository: therealityreport/trr-app
Length of output: 35090
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file='apps/web/src/lib/admin/api-references/generator.ts'
echo '--- function ---'
nl -ba "$file" | sed -n '177,220p'
echo '--- occurrences of nullish coalescing in template expressions under apps/web/src ---'
rg -n '\$\{[^}]*\?\?[^}]*\}' apps/web/src --glob '*.ts' --glob '*.tsx' -g '!**/tests/**' || true
echo '--- occurrences of optional chaining + nullish coalescing in template expressions under apps/web/src ---'
rg -n '\$\{[^}]*\?\.[^}]*\?\?[^}]*\}' apps/web/src --glob '*.ts' --glob '*.tsx' -g '!**/tests/**' || true
echo '--- occurrences of query-builder-style templates ---'
rg -n '\?\s*`?\?\$\{' apps/web/src --glob '*.ts' --glob '*.tsx' -g '!**/tests/**' || trueRepository: therealityreport/trr-app
Length of output: 219
Require a backtick in the query-builder heuristic
The second regex also matches plain ?? expressions like ${obj?.id ?? 'x'}, so stripPathQueryTemplate can truncate a legitimate path segment and drop everything after it. Require the backtick here.
Proposed fix
- /^\s*[^}]*\?\s*`?\?/.test(expression) ||
+ /^\s*[^}]*\?\s*`\s*\?/.test(expression) ||📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function stripPathQueryTemplate(raw: string): string { | |
| let templateDepth = 0; | |
| for (let index = 0; index < raw.length; index += 1) { | |
| const current = raw[index]; | |
| const next = raw[index + 1]; | |
| if (current === "$" && next === "{") { | |
| if (templateDepth === 0) { | |
| const expression = raw.slice(index + 2); | |
| if ( | |
| /^\s*(?:query|search|searchParams|queryParams|urlSearchParams)\b/i.test(expression) || | |
| /^\s*[^}]*\?\s*`?\?/.test(expression) || | |
| /^\s*[^}]*:\s*`?\s*`/.test(expression) | |
| ) { | |
| return raw.slice(0, index); | |
| } | |
| } | |
| templateDepth += 1; | |
| index += 1; | |
| continue; | |
| } | |
| if (current === "}" && templateDepth > 0) { | |
| templateDepth -= 1; | |
| continue; | |
| } | |
| if (templateDepth === 0 && (current === "?" || current === "#")) { | |
| return raw.slice(0, index); | |
| } | |
| } | |
| return raw; | |
| } | |
| function stripPathQueryTemplate(raw: string): string { | |
| let templateDepth = 0; | |
| for (let index = 0; index < raw.length; index += 1) { | |
| const current = raw[index]; | |
| const next = raw[index + 1]; | |
| if (current === "$" && next === "{") { | |
| if (templateDepth === 0) { | |
| const expression = raw.slice(index + 2); | |
| if ( | |
| /^\s*(?:query|search|searchParams|queryParams|urlSearchParams)\b/i.test(expression) || | |
| /^\s*[^}]*\?\s*`\s*\?/.test(expression) || | |
| /^\s*[^}]*:\s*`?\s*`/.test(expression) | |
| ) { | |
| return raw.slice(0, index); | |
| } | |
| } | |
| templateDepth += 1; | |
| index += 1; | |
| continue; | |
| } | |
| if (current === "}" && templateDepth > 0) { | |
| templateDepth -= 1; | |
| continue; | |
| } | |
| if (templateDepth === 0 && (current === "?" || current === "#")) { | |
| return raw.slice(0, index); | |
| } | |
| } | |
| return raw; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/lib/admin/api-references/generator.ts` around lines 177 - 206,
The query-builder heuristic in stripPathQueryTemplate is too broad and can match
plain nullish-coalescing expressions like ${obj?.id ?? 'x'}, causing valid path
content to be truncated. Tighten the second regex in stripPathQueryTemplate so
it only treats template-based query builders as matches by requiring a backtick
in that branch, while preserving the existing checks for
query/search/queryParams/urlSearchParams handling.
| function getDefaultEnv(): FlagEnv { | ||
| return typeof process === "undefined" ? EMPTY_ENV : process.env; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Locate consumers of the flag helpers to confirm client vs server usage
rg -n --type=ts --type=tsx -B2 -A5 'getTrrAppFlagValue|getTrrAppFlags\(' apps/web/src
rg -n '"use client"' apps/web/src/app/admin/social/page.tsxRepository: therealityreport/trr-app
Length of output: 206
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- trr-app-flags.ts ---'
nl -ba apps/web/src/lib/trr-app-flags.ts | sed -n '1,220p'
echo '--- usages ---'
rg -n -B2 -A6 'getTrrAppFlagValue|getTrrAppFlags' apps/web/src || true
echo '--- admin/social page ---'
nl -ba apps/web/src/app/admin/social/page.tsx | sed -n '1,220p'Repository: therealityreport/trr-app
Length of output: 227
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- trr-app-flags.ts ---'
sed -n '1,220p' apps/web/src/lib/trr-app-flags.ts
echo '--- usages ---'
rg -n -B2 -A6 'getTrrAppFlagValue|getTrrAppFlags' apps/web/src || true
echo '--- admin/social page ---'
sed -n '1,220p' apps/web/src/app/admin/social/page.tsxRepository: therealityreport/trr-app
Length of output: 12557
Client flag lookups need literal env access. apps/web/src/app/admin/social/page.tsx is a "use client" component, so getTrrAppFlags() runs in the browser. With getDefaultEnv() returning process.env, env[definition.envVar] stays dynamic and these NEXT_PUBLIC_* flags fall back to the hardcoded defaults. Read each key with a literal process.env.NEXT_PUBLIC_... access or pass the resolved values in from the server.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/lib/trr-app-flags.ts` around lines 48 - 50, The client-side flag
lookup is using dynamic env access, so `getTrrAppFlags()` in the `"use client"`
flow cannot reliably read `NEXT_PUBLIC_*` values. Update `getDefaultEnv()` and
the flag resolution path in `trr-app-flags.ts` so client reads use literal
`process.env.NEXT_PUBLIC_...` access (or receive resolved values from the
server) instead of `env[definition.envVar]`. Keep the fix focused around
`getDefaultEnv()` and `getTrrAppFlags()` so the browser sees the actual public
flag values.
| vercel_deploy_targets_production() { | ||
| local arg | ||
| local after_deploy=0 | ||
| local option_value_for="" | ||
| local skip_next=0 | ||
|
|
||
| for arg in "$@"; do | ||
| if [[ "$skip_next" -eq 1 ]]; then | ||
| if [[ "$after_deploy" -eq 1 && "$option_value_for" == "--target" && "$arg" == "production" ]]; then | ||
| return 0 | ||
| fi | ||
| skip_next=0 | ||
| option_value_for="" | ||
| continue | ||
| fi | ||
|
|
||
| if [[ "$after_deploy" -eq 1 ]]; then | ||
| case "$arg" in | ||
| --prod | --prod=true | --target=production) | ||
| return 0 | ||
| ;; | ||
| --target) | ||
| skip_next=1 | ||
| option_value_for="$arg" | ||
| ;; | ||
| --*=*) | ||
| continue | ||
| ;; | ||
| -*) | ||
| if option_takes_value "$arg"; then | ||
| skip_next=1 | ||
| option_value_for="$arg" | ||
| fi | ||
| ;; | ||
| esac | ||
| continue | ||
| fi | ||
|
|
||
| case "$arg" in | ||
| --*=*) | ||
| continue | ||
| ;; | ||
| -*) | ||
| if option_takes_value "$arg"; then | ||
| skip_next=1 | ||
| option_value_for="$arg" | ||
| fi | ||
| continue | ||
| ;; | ||
| esac | ||
|
|
||
| [[ "$arg" == "deploy" ]] && after_deploy=1 | ||
| done | ||
|
|
||
| return 1 | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
git ls-files scripts/vercel.sh
wc -l scripts/vercel.sh
ast-grep outline scripts/vercel.sh --view expanded
rg -n "vercel_deploy_targets_production|first_vercel_command|option_takes_value|TRR_VERCEL_ALLOW_PROD|vercel_project_guard_required" scripts/vercel.sh
sed -n '1,220p' scripts/vercel.sh
sed -n '220,360p' scripts/vercel.shRepository: therealityreport/trr-app
Length of output: 9172
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '560,610p' scripts/vercel.sh
sed -n '1,220p' scripts/vercel.sh | nl -ba | sed -n '170,260p'Repository: therealityreport/trr-app
Length of output: 1885
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '560,610p' scripts/vercel.sh
nl -ba scripts/vercel.sh | sed -n '177,256p'Repository: therealityreport/trr-app
Length of output: 1885
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
path = Path('scripts/vercel.sh')
for i, line in enumerate(path.read_text().splitlines(), 1):
if 177 <= i <= 256:
print(f"{i:4d}: {line}")
PYRepository: therealityreport/trr-app
Length of output: 2209
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "scripts/vercel\.sh|vercel\.sh|TRR_VERCEL_ALLOW_PROD|--prod|--target=production|deploy" -g '!*.min.*' .Repository: therealityreport/trr-app
Length of output: 33971
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "scripts/vercel\.sh|vercel\.sh|TRR_VERCEL_ALLOW_PROD|--prod|--target=production|deploy" .Repository: therealityreport/trr-app
Length of output: 33971
🌐 Web query:
Vercel CLI deploy command omit deploy subcommand --prod top-level flag documentation
💡 Result:
In the Vercel CLI, the deploy subcommand is the default command, meaning it can be omitted [1][2]. For example, running vercel in a project directory is equivalent to running vercel deploy [1]. The --prod flag is used to create a production deployment [2][3]. When you omit the deploy subcommand, you can still apply the --prod flag directly to the base vercel command to trigger a production deployment [3]. For example: - vercel --prod is equivalent to vercel deploy --prod [2][3]. By default, running vercel (or vercel deploy) without the --prod flag creates a preview deployment [3]. Using the --prod flag explicitly informs Vercel to create a production deployment, which updates your production domains upon success [3].
Citations:
- 1: https://github.com/vercel/vercel/blob/22ae14af/packages/cli/src/commands/deploy/command.ts
- 2: https://github.com/vercel/vercel/blob/7e64cc27/packages/cli/src/commands/deploy/command.ts
- 3: https://vercel.com/docs/deployments/environments.md
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
path = Path('scripts/vercel.sh')
for i, line in enumerate(path.read_text().splitlines(), 1):
if 608 <= i <= 640:
print(f"{i:4d}: {line}")
PYRepository: therealityreport/trr-app
Length of output: 315
Production deploy guard should treat bare vercel as deploy (scripts/vercel.sh:201-256,601-605) vercel_deploy_targets_production() only starts scanning for --prod/--target=production after a literal deploy, but Vercel accepts vercel --prod as a production deploy too. That lets ./scripts/vercel.sh --prod skip the TRR_VERCEL_ALLOW_PROD check.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/vercel.sh` around lines 201 - 256, The production-deploy detection in
vercel_deploy_targets_production() only begins after seeing a literal deploy
subcommand, so bare vercel invocations like vercel --prod are missed and bypass
the TRR_VERCEL_ALLOW_PROD guard. Update the argument scan in
vercel_deploy_targets_production() to treat the no-subcommand form of vercel as
equivalent to deploy when checking for production flags, while still preserving
the existing handling for deploy and option values. Make sure the logic in
scripts/vercel.sh that gates production deploys uses this updated detection
path.
Summary
Validation
git diff --check origin/main...HEADpnpm -C apps/web exec vitest run tests/admin-social-page-auth-bypass.test.tsxpnpm -C apps/web exec tsc -p tsconfig.typecheck.json --noEmitpnpm run web:validate:quickNotes
Summary by CodeRabbit
New Features
Bug Fixes
Chores