Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ make dev-portless
```

Then open the public app at `https://trr.localhost` or the admin dashboard at
`https://admin.trr.localhost/admin`. See
`https://admin.trr.localhost`. See
`../../docs/workspace/portless-clean-urls.md` for the shared local URL runbook.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
Expand Down Expand Up @@ -61,7 +61,7 @@ Notes
Use a dedicated local admin origin to mirror production isolation:

- Public app: `https://trr.localhost`
- Admin app: `https://admin.trr.localhost/admin`
- Admin app: `https://admin.trr.localhost`

Add these values in `.env.local`:

Expand Down
2 changes: 2 additions & 0 deletions apps/web/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ const eslintConfig = [
"build/**",
"test-results/**",
"next-env.d.ts",
"src/app/admin/trr-shows/**",
"src/components/admin/SocialAccountProfilePage.tsx",
],
},
{
Expand Down
13 changes: 3 additions & 10 deletions apps/web/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ const nextConfig: NextConfig = {
reactStrictMode: true,
typedRoutes: TYPED_ROUTES_ENABLED,
distDir: DIST_DIR,
allowedDevOrigins: IS_DEV
? ["admin.trr.localhost", "trr.localhost", "api.trr.localhost", "localhost"]
: undefined,
experimental: BUILD_WORKER_COUNT
? {
cpus: BUILD_WORKER_COUNT,
Expand Down Expand Up @@ -153,16 +156,6 @@ const nextConfig: NextConfig = {
destination: "/:showId/social/s:seasonNumber/w:weekIndex",
permanent: false,
},
{
source: "/shows/:showId((?!settings$)[^/]+)/:rest*",
destination: "/:showId/:rest*",
permanent: false,
},
{
source: "/shows/:showId((?!settings$)[^/]+)",
destination: "/:showId",
permanent: false,
},
];
},
async rewrites() {
Expand Down
41 changes: 22 additions & 19 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@
"typecheck": "rm -rf .next/dev/types .next/types && tsc -p tsconfig.typecheck.json --noEmit",
"typecheck:fandom": "tsc -p tsconfig.typecheck.fandom.json --noEmit",
"validate:quick": "pnpm run generated:check && pnpm exec vitest run -c vitest.config.mts tests/validation.test.ts tests/shared-env-contract.test.ts tests/safe-next-build.test.ts",
"lint": "eslint",
"lint": "NODE_OPTIONS=--max-old-space-size=8192 eslint",
"clean": "rm -rf .next",
"generated:check": "node scripts/generate-admin-api-references.mjs --check && node scripts/generate-brand-font-artifacts.mjs --check",
"generate:brand-font-artifacts": "node scripts/generate-brand-font-artifacts.mjs",
"generate:brand-font-artifacts:with-glyphs": "node scripts/generate-brand-font-artifacts.mjs --with-glyph-comparison",
"brand-fonts:visual-similarity": "node scripts/brand-font-visual-similarity.mjs",
"brand-fonts:glyph-comparison": "node scripts/brand-font-glyph-comparison.mjs",
"perf:admin:api": "node scripts/perf-admin-api.mjs",
"design-docs:classify": "node scripts/design-docs/classify-publisher-patterns.mjs",
"design-docs:extract-navigation": "node scripts/design-docs/extract-navigation.mjs",
"design-docs:nyt-games:media": "node scripts/design-docs/generate-nyt-games-media-artifacts.mjs",
Expand All @@ -45,38 +46,40 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@firebase/app": "0.14.13",
"@firebase/app": "0.15.0",
"@firebase/component": "0.7.3",
"@firebase/firestore": "4.15.0",
"@firebase/firestore": "4.16.0",
"@firebase/logger": "0.5.1",
"@firebase/util": "1.15.1",
"@sentry/nextjs": "^10.58.0",
"@supabase/supabase-js": "^2.108.2",
"@vercel/functions": "^3.7.1",
"@sentry/nextjs": "^10.62.0",
"@supabase/supabase-js": "^2.110.0",
"@vercel/analytics": "^2.0.1",
"@vercel/functions": "^3.7.4",
"@vercel/speed-insights": "^2.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"firebase": "^12.14.0",
"firebase-admin": "^14.0.0",
"framer-motion": "^12.40.0",
"firebase": "^12.15.0",
"firebase-admin": "^14.1.0",
"framer-motion": "^12.42.1",
"html-to-image": "^1.11.13",
"lucide-react": "^1.18.0",
"lucide-react": "^1.22.0",
"next": "16.2.9",
"pg": "^8.21.0",
"radix-ui": "^1.5.0",
"pg": "^8.22.0",
"radix-ui": "^1.6.0",
"react": "19.2.7",
"react-dom": "19.2.7",
"shadcn": "^4.11.0",
"shadcn": "^4.12.0",
"tailwind-merge": "^3.6.0",
"tw-animate-css": "^1.4.0",
"undici": "^8.4.1"
"undici": "^8.5.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.5",
"@playwright/test": "^1.61.0",
"@tailwindcss/postcss": "^4.3.1",
"@playwright/test": "^1.61.1",
"@tailwindcss/postcss": "^4.3.2",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/node": "^25.9.3",
"@types/node": "^25.9.4",
"@types/pg": "^8.20.0",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
Expand All @@ -85,9 +88,9 @@
"eslint": "^9.39.4",
"eslint-config-next": "16.2.9",
"jsdom": "^29.1.1",
"tailwindcss": "^4.3.1",
"tailwindcss": "^4.3.2",
"typescript": "^5.9.3",
"vite": "^7.3.5",
"vite": "^7.3.6",
"vite-tsconfig-paths": "^6.1.1",
"vitest": "^4.1.9",
"vitest-axe": "^0.1.0",
Expand Down
4 changes: 2 additions & 2 deletions apps/web/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { defineConfig, devices } from "@playwright/test";

const PORT = Number.parseInt(process.env.PLAYWRIGHT_PORT ?? "3200", 10);
const LIVE_MODE = process.env.E2E_CAST_LIVE === "1";
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL ?? `http://127.0.0.1:${PORT}`;
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL ?? `http://localhost:${PORT}`;
const STORAGE_STATE = process.env.PLAYWRIGHT_STORAGE_STATE;

export default defineConfig({
Expand Down Expand Up @@ -30,7 +30,7 @@ export default defineConfig({
webServer: LIVE_MODE
? undefined
: {
command: `ADMIN_APP_ORIGIN=http://127.0.0.1:${PORT} ADMIN_APP_HOSTS=127.0.0.1,localhost,admin.localhost NEXT_PUBLIC_DEV_ADMIN_BYPASS=true NEXT_DIST_DIR=.next-e2e pnpm exec next dev --webpack -p ${PORT}`,
command: `ADMIN_APP_ORIGIN=http://localhost:${PORT} ADMIN_APP_HOSTS=localhost,127.0.0.1,admin.localhost NEXT_PUBLIC_DEV_ADMIN_BYPASS=true NEXT_DIST_DIR=.next-e2e pnpm exec next dev --webpack -p ${PORT}`,
url: BASE_URL,
timeout: 240_000,
reuseExistingServer: false,
Expand Down
190 changes: 190 additions & 0 deletions apps/web/scripts/perf-admin-api.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const appRoot = resolve(__dirname, "..");
const workspaceRoot = resolve(appRoot, "../../..");
const defaultOutputRoot = resolve(workspaceRoot, ".artifacts/perf/autocannon");
const defaultCaPath = `${process.env.HOME ?? ""}/.portless/ca.pem`;

const presets = {
"social-snapshot": "/api/admin/trr-api/social/profiles/instagram/bravotv/snapshot",
"social-catalog-detail": "/api/admin/trr-api/social/profiles/instagram/bravotv/snapshot?detail=catalog",
"comments-summary": "/api/admin/trr-api/social/profiles/instagram/bravotv/comments/summary",
"cast-data": "/api/admin/trr-api/shows/rhoslc/cast",
"credits-data": "/api/admin/trr-api/shows/rhoslc/credits",
"cast-credits": "/api/admin/trr-api/shows/rhoslc/cast-credits",
};

function printHelp() {
console.log(`Usage: pnpm run perf:admin:api -- [options]

Runs a low-concurrency autocannon benchmark against a clean Portless admin URL.

Options:
--preset <name> Preset route. Default: social-snapshot
--url <url> Explicit https://admin.trr.localhost/... URL override
--connections <n> Autocannon connections. Default: 2
--duration <seconds> Autocannon duration. Default: 10
--max-p95-ms <ms> Fail when parsed p95 latency exceeds this value
--max-p99-ms <ms> Fail when parsed p99 latency exceeds this value
--out <dir> Output root. Default: .artifacts/perf/autocannon
--print-target Print the resolved target and exit
--help Show this help

Presets: ${Object.keys(presets).join(", ")}`);
}

function readArgs(argv) {
const options = {
preset: "social-snapshot",
connections: "2",
duration: "10",
out: defaultOutputRoot,
};

for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
const next = () => {
const value = argv[index + 1];
if (!value || value.startsWith("--")) {
throw new Error(`${arg} requires a value`);
}
index += 1;
return value;
};

if (arg === "--help" || arg === "-h") options.help = true;
else if (arg === "--") continue;
else if (arg === "--print-target") options.printTarget = true;
else if (arg === "--preset") options.preset = next();
else if (arg === "--url") options.url = next();
else if (arg === "--connections") options.connections = next();
else if (arg === "--duration") options.duration = next();
else if (arg === "--max-p95-ms") options.maxP95Ms = Number(next());
else if (arg === "--max-p99-ms") options.maxP99Ms = Number(next());
else if (arg === "--out") options.out = resolve(next());
else throw new Error(`Unknown option: ${arg}`);
}

return options;
}

function resolveTarget(options) {
if (options.url) {
const parsed = new URL(options.url);
if (parsed.protocol !== "https:" || parsed.hostname !== "admin.trr.localhost") {
throw new Error("--url must use https://admin.trr.localhost");
}
return parsed.toString();
}

const route = presets[options.preset];
if (!route) {
throw new Error(`Unknown preset: ${options.preset}`);
}
return new URL(route, "https://admin.trr.localhost").toString();
}

function percentileMs(result, key) {
const latency = result.latency;
if (!latency || typeof latency !== "object") return null;
const value = latency[key] ?? latency[`p${key}`];
return typeof value === "number" ? value : null;
}

try {
const options = readArgs(process.argv.slice(2));
if (options.help) {
printHelp();
process.exit(0);
}

const target = resolveTarget(options);
if (options.printTarget) {
console.log(target);
process.exit(0);
}

const timestamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+$/, "Z");
const outputDir = resolve(options.out, timestamp);
mkdirSync(outputDir, { recursive: true });

const env = { ...process.env };
if (!env.NODE_EXTRA_CA_CERTS && defaultCaPath.startsWith("/")) {
env.NODE_EXTRA_CA_CERTS = defaultCaPath;
}

const result = spawnSync(
"npx",
["--yes", "autocannon", "--connections", options.connections, "--duration", options.duration, "--latency", "--json", target],
{
cwd: appRoot,
env,
encoding: "utf8",
},
);

const resultPath = resolve(outputDir, "result.json");
writeFileSync(resultPath, result.stdout || "", "utf8");
if (result.stderr) {
writeFileSync(resolve(outputDir, "stderr.log"), result.stderr, "utf8");
}
writeFileSync(
resolve(outputDir, "metadata.json"),
JSON.stringify(
{
target,
preset: options.url ? null : options.preset,
connections: Number(options.connections),
duration_seconds: Number(options.duration),
generated_at: new Date().toISOString(),
},
null,
2,
),
"utf8",
);

const latestPath = resolve(options.out, "latest");
rmSync(latestPath, { force: true, recursive: true });
symlinkSync(outputDir, latestPath, "dir");

if (result.status !== 0) {
process.stderr.write(result.stderr || "autocannon failed\n");
process.exit(result.status ?? 1);
}

let parsed;
try {
parsed = JSON.parse(result.stdout);
} catch {
throw new Error(`autocannon output was not valid JSON: ${resultPath}`);
}

const p95 = percentileMs(parsed, "p95");
const p99 = percentileMs(parsed, "p99");
const failures = [];
if (typeof options.maxP95Ms === "number" && p95 !== null && p95 > options.maxP95Ms) {
failures.push(`p95 ${p95}ms exceeded ${options.maxP95Ms}ms`);
}
if (typeof options.maxP99Ms === "number" && p99 !== null && p99 > options.maxP99Ms) {
failures.push(`p99 ${p99}ms exceeded ${options.maxP99Ms}ms`);
}

console.log(`autocannon result: ${resultPath}`);
console.log(`target: ${target}`);
if (p95 !== null || p99 !== null) {
console.log(`latency: p95=${p95 ?? "n/a"}ms p99=${p99 ?? "n/a"}ms`);
}
if (failures.length > 0) {
console.error(failures.join("\n"));
process.exit(1);
}
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ADMIN_ROOT_PATH } from "@/lib/admin/admin-route-paths";
const DEV_DASHBOARD_ROUTES = [
{ href: "/dev-dashboard", label: "Overview" },
{ href: "/dev-dashboard/skills-and-agents", label: "Skills & Agents" },
{ href: "/dev-dashboard/instagram-catalog-backfill-mockup", label: "Catalog Backfill" },
] as const;

interface DevDashboardShellProps {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { InstagramCatalogBackfillMockupView } from "@/components/admin/instagram/InstagramCatalogBackfillMockupView";

export default function InstagramCatalogBackfillMockupPage() {
return (
<InstagramCatalogBackfillMockupView
platform="instagram"
handle="bravotv"
canonicalCatalogUrl="/social/instagram/bravotv/catalog"
variantLabel="Mockup"
/>
);
}
Loading
Loading