@percepta/create 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +93 -0
  2. package/dist/chunk-GEVZERMP.js +108 -0
  3. package/dist/chunk-R4FWPE4A.js +49 -0
  4. package/dist/chunk-WMJT7CB5.js +57 -0
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.js +974 -0
  7. package/dist/init-Z4VGBHAK.js +96 -0
  8. package/dist/status-MITGDLTT.js +76 -0
  9. package/dist/sync-J4SFZHDX.js +136 -0
  10. package/dist/upstream-AQI7P4EU.js +144 -0
  11. package/package.json +58 -0
  12. package/template-versions.json +4 -0
  13. package/templates/library/README.md +30 -0
  14. package/templates/library/eslint.config.js +10 -0
  15. package/templates/library/gitignore.template +18 -0
  16. package/templates/library/package.json.template +29 -0
  17. package/templates/library/src/index.ts +9 -0
  18. package/templates/library/tsconfig.json +19 -0
  19. package/templates/monorepo/README.md +41 -0
  20. package/templates/monorepo/eslint.config.js +10 -0
  21. package/templates/monorepo/gitignore.template +31 -0
  22. package/templates/monorepo/npmrc.template +4 -0
  23. package/templates/monorepo/package.json.template +25 -0
  24. package/templates/monorepo/packages/.gitkeep +0 -0
  25. package/templates/monorepo/pnpm-workspace.yaml +2 -0
  26. package/templates/monorepo/tsconfig.json +16 -0
  27. package/templates/webapp/.claude/commands/sync.md +19 -0
  28. package/templates/webapp/.claude/commands/upstream.md +17 -0
  29. package/templates/webapp/.dockerignore +59 -0
  30. package/templates/webapp/.gitattributes +1 -0
  31. package/templates/webapp/.github/workflows/__APP_NAME__-ryvn-release.yaml +114 -0
  32. package/templates/webapp/.github/workflows/__APP_NAME__-terraform.yml +28 -0
  33. package/templates/webapp/.github/workflows/ci.yml +149 -0
  34. package/templates/webapp/.node-version +2 -0
  35. package/templates/webapp/.prettierrc.mjs +5 -0
  36. package/templates/webapp/AGENTS.md +240 -0
  37. package/templates/webapp/Dockerfile +64 -0
  38. package/templates/webapp/README.md +200 -0
  39. package/templates/webapp/agent-skills/database.md +140 -0
  40. package/templates/webapp/agent-skills/deploy.md +94 -0
  41. package/templates/webapp/agent-skills/inngest.md +147 -0
  42. package/templates/webapp/agent-skills/langfuse.md +117 -0
  43. package/templates/webapp/agent-skills/oneshot.md +216 -0
  44. package/templates/webapp/agent-skills/ryvn.md +25 -0
  45. package/templates/webapp/deploy/README.md +39 -0
  46. package/templates/webapp/deploy/ryvn/__APP_NAME__.service.yaml +11 -0
  47. package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__.env.percepta-test.serviceinstallation.yaml +121 -0
  48. package/templates/webapp/docker-compose.yml +19 -0
  49. package/templates/webapp/drizzle.config.ts +30 -0
  50. package/templates/webapp/env.example.template +44 -0
  51. package/templates/webapp/eslint.config.mjs +52 -0
  52. package/templates/webapp/gitignore.template +53 -0
  53. package/templates/webapp/next.config.ts +8 -0
  54. package/templates/webapp/npmrc.template +4 -0
  55. package/templates/webapp/package.json.template +122 -0
  56. package/templates/webapp/postcss.config.mjs +5 -0
  57. package/templates/webapp/scripts/create-user.ts +47 -0
  58. package/templates/webapp/scripts/migrate.ts +18 -0
  59. package/templates/webapp/scripts/seed.ts +62 -0
  60. package/templates/webapp/scripts/setup-database.ts +57 -0
  61. package/templates/webapp/scripts/setup-readonly-user.ts +193 -0
  62. package/templates/webapp/scripts/start.sh +52 -0
  63. package/templates/webapp/src/app/(app)/layout.tsx +21 -0
  64. package/templates/webapp/src/app/(app)/page.tsx +30 -0
  65. package/templates/webapp/src/app/(auth)/auth/signin/CredentialsSignInForm.tsx +103 -0
  66. package/templates/webapp/src/app/(auth)/auth/signin/page.tsx +30 -0
  67. package/templates/webapp/src/app/(auth)/layout.tsx +15 -0
  68. package/templates/webapp/src/app/api/auth/[...all]/route.ts +4 -0
  69. package/templates/webapp/src/app/api/healthz/route.ts +10 -0
  70. package/templates/webapp/src/app/api/inngest/route.ts +31 -0
  71. package/templates/webapp/src/app/api/readyz/route.ts +31 -0
  72. package/templates/webapp/src/app/api/trpc/[trpc]/route.ts +21 -0
  73. package/templates/webapp/src/app/favicon.ico +0 -0
  74. package/templates/webapp/src/app/global-error.tsx +27 -0
  75. package/templates/webapp/src/app/layout.tsx +18 -0
  76. package/templates/webapp/src/components/FaroProvider.tsx +37 -0
  77. package/templates/webapp/src/components/Header.tsx +70 -0
  78. package/templates/webapp/src/components/Providers.tsx +45 -0
  79. package/templates/webapp/src/components/form/FormItem.tsx +82 -0
  80. package/templates/webapp/src/config/clientEnvConfig.ts +11 -0
  81. package/templates/webapp/src/config/getEnvConfig.ts +62 -0
  82. package/templates/webapp/src/config/isDev.ts +7 -0
  83. package/templates/webapp/src/drizzle/db.ts +28 -0
  84. package/templates/webapp/src/drizzle/migrations/0000_eager_grandmaster.sql +57 -0
  85. package/templates/webapp/src/drizzle/migrations/meta/0000_snapshot.json +376 -0
  86. package/templates/webapp/src/drizzle/migrations/meta/_journal.json +13 -0
  87. package/templates/webapp/src/drizzle/schema/auth/accounts.ts +33 -0
  88. package/templates/webapp/src/drizzle/schema/auth/sessions.ts +25 -0
  89. package/templates/webapp/src/drizzle/schema/auth/users.ts +38 -0
  90. package/templates/webapp/src/drizzle/schema/auth/verifications.ts +19 -0
  91. package/templates/webapp/src/drizzle/schema/index.ts +4 -0
  92. package/templates/webapp/src/drizzle/schema/utils/jsonbFromZod.ts +25 -0
  93. package/templates/webapp/src/instrumentation.ts +35 -0
  94. package/templates/webapp/src/lib/auth/index.ts +85 -0
  95. package/templates/webapp/src/lib/auth-client.ts +6 -0
  96. package/templates/webapp/src/lib/trpc.ts +15 -0
  97. package/templates/webapp/src/server/api/root.ts +5 -0
  98. package/templates/webapp/src/server/trpc.ts +61 -0
  99. package/templates/webapp/src/services/AuthContextService.ts +63 -0
  100. package/templates/webapp/src/services/DatabaseService.ts +54 -0
  101. package/templates/webapp/src/services/inngest/InngestFunctionCollection.ts +5 -0
  102. package/templates/webapp/src/services/inngest/InngestService.ts +71 -0
  103. package/templates/webapp/src/services/inngest/events/AppEvents.ts +34 -0
  104. package/templates/webapp/src/services/inngest/events/payloads/ExampleEventPayload.ts +14 -0
  105. package/templates/webapp/src/services/langfuse/LangfuseService.ts +80 -0
  106. package/templates/webapp/src/services/logger/AppLogger.ts +61 -0
  107. package/templates/webapp/src/services/logger/withRequestContext.ts +27 -0
  108. package/templates/webapp/src/services/observability/initFaro.ts +22 -0
  109. package/templates/webapp/src/startup-checks.ts +32 -0
  110. package/templates/webapp/src/styles/globals.css +27 -0
  111. package/templates/webapp/src/utils/__tests__/cn.test.ts +20 -0
  112. package/templates/webapp/src/utils/cn.ts +6 -0
  113. package/templates/webapp/src/utils/syncInngestApp.ts +62 -0
  114. package/templates/webapp/terraform/README.md +147 -0
  115. package/templates/webapp/terraform/deploy.sh +97 -0
  116. package/templates/webapp/terraform/main.tf +101 -0
  117. package/templates/webapp/terraform/modules/cloudtrail/main.tf +27 -0
  118. package/templates/webapp/terraform/modules/cloudtrail/outputs.tf +10 -0
  119. package/templates/webapp/terraform/modules/cloudtrail/variables.tf +15 -0
  120. package/templates/webapp/terraform/modules/networking/main.tf +118 -0
  121. package/templates/webapp/terraform/modules/networking/outputs.tf +38 -0
  122. package/templates/webapp/terraform/modules/networking/variables.tf +24 -0
  123. package/templates/webapp/terraform/modules/rds/main.tf +227 -0
  124. package/templates/webapp/terraform/modules/rds/outputs.tf +73 -0
  125. package/templates/webapp/terraform/modules/rds/variables.tf +61 -0
  126. package/templates/webapp/terraform/modules/s3-logging/main.tf +148 -0
  127. package/templates/webapp/terraform/modules/s3-logging/outputs.tf +10 -0
  128. package/templates/webapp/terraform/modules/s3-logging/variables.tf +16 -0
  129. package/templates/webapp/terraform/modules/secrets/main.tf +39 -0
  130. package/templates/webapp/terraform/modules/secrets/outputs.tf +9 -0
  131. package/templates/webapp/terraform/modules/secrets/variables.tf +51 -0
  132. package/templates/webapp/terraform/outputs.tf +102 -0
  133. package/templates/webapp/terraform/providers.tf +32 -0
  134. package/templates/webapp/terraform/terraform.tfvars.example +65 -0
  135. package/templates/webapp/terraform/variables.tf +129 -0
  136. package/templates/webapp/tsconfig.json +14 -0
  137. package/templates/webapp/vitest.config.ts +9 -0
  138. package/templates/webapp/vitest.setup.ts +5 -0
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ import {
4
+ GetSecretValueCommand,
5
+ SecretsManagerClient,
6
+ } from "@aws-sdk/client-secrets-manager";
7
+ import { loadEnvConfig } from "@next/env";
8
+ import { Pool } from "pg";
9
+ import { getEnvConfig } from "../src/config/getEnvConfig";
10
+
11
+ interface ReadonlyCredentials {
12
+ username: string;
13
+ password: string;
14
+ host: string;
15
+ port: number;
16
+ database: string;
17
+ engine: string;
18
+ }
19
+
20
+ async function getReadonlyCredentials(): Promise<ReadonlyCredentials> {
21
+ const client = new SecretsManagerClient({});
22
+
23
+ // Get the secret name from environment
24
+ // Example format: rds-{app}-readonly-{name}-{environment}-*
25
+ const { READONLY_SECRET_NAME: secretName } = getEnvConfig();
26
+ if (secretName == null) {
27
+ throw new Error("READONLY_SECRET_NAME environment variable not set");
28
+ }
29
+
30
+ const command = new GetSecretValueCommand({ SecretId: secretName });
31
+ const response = await client.send(command);
32
+
33
+ if (!response.SecretString) {
34
+ throw new Error("Secret value is empty");
35
+ }
36
+
37
+ return JSON.parse(response.SecretString) as ReadonlyCredentials;
38
+ }
39
+
40
+ async function setupReadonlyUser(): Promise<number> {
41
+ console.log("🔐 Fetching readonly user credentials from Secrets Manager...");
42
+
43
+ let credentials: ReadonlyCredentials;
44
+ try {
45
+ credentials = await getReadonlyCredentials();
46
+ console.log(`✅ Retrieved credentials for user: ${credentials.username}`);
47
+ } catch (error) {
48
+ console.error(
49
+ "❌ Failed to retrieve credentials from Secrets Manager:",
50
+ error,
51
+ );
52
+ return 1;
53
+ }
54
+
55
+ // Connect as master user
56
+ const {
57
+ DATABASE_HOST: host,
58
+ DATABASE_PORT: port,
59
+ DATABASE_USERNAME: user,
60
+ DATABASE_PASSWORD: password,
61
+ DATABASE_NAME: database,
62
+ DATABASE_USE_SSL: useSSL,
63
+ } = getEnvConfig();
64
+ const masterPool = new Pool({
65
+ host,
66
+ port,
67
+ user,
68
+ password,
69
+ database,
70
+ ssl: useSSL ? { rejectUnauthorized: false } : false,
71
+ });
72
+
73
+ let client;
74
+ try {
75
+ client = await masterPool.connect();
76
+ console.log("✅ Connected to database as master user");
77
+
78
+ // Check if readonly user already exists
79
+ const userExistsResult = await client.query(
80
+ "SELECT 1 FROM pg_roles WHERE rolname = $1",
81
+ [credentials.username],
82
+ );
83
+
84
+ let userCreated = false;
85
+ if (userExistsResult.rows.length === 0) {
86
+ // Create the readonly user
87
+ console.log(`📝 Creating user: ${credentials.username}`);
88
+ await client.query(
89
+ `
90
+ CREATE USER ${credentials.username} WITH LOGIN PASSWORD $1
91
+ `,
92
+ [credentials.password],
93
+ );
94
+ userCreated = true;
95
+ console.log(`✅ User ${credentials.username} created`);
96
+ } else {
97
+ // Update password to ensure it matches Secrets Manager
98
+ console.log(
99
+ `📝 Updating password for existing user: ${credentials.username}`,
100
+ );
101
+ await client.query(
102
+ `
103
+ ALTER USER ${credentials.username} WITH PASSWORD $1
104
+ `,
105
+ [credentials.password],
106
+ );
107
+ console.log(`✅ Password updated for ${credentials.username}`);
108
+ }
109
+
110
+ // Grant CONNECT privilege on database
111
+ await client.query(`
112
+ GRANT CONNECT ON DATABASE ${database} TO ${credentials.username}
113
+ `);
114
+ console.log("✅ Granted CONNECT on database");
115
+
116
+ // Get all schemas (excluding system schemas)
117
+ const schemasResult = await client.query<{ schema_name: string }>(`
118
+ SELECT schema_name
119
+ FROM information_schema.schemata
120
+ WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
121
+ `);
122
+
123
+ for (const row of schemasResult.rows) {
124
+ const schemaName = row.schema_name;
125
+ console.log(`📝 Configuring access for schema: ${schemaName}`);
126
+
127
+ // Grant USAGE on schema
128
+ await client.query(`
129
+ GRANT USAGE ON SCHEMA ${schemaName} TO ${credentials.username}
130
+ `);
131
+
132
+ // Grant SELECT on all existing tables in schema
133
+ await client.query(`
134
+ GRANT SELECT ON ALL TABLES IN SCHEMA ${schemaName} TO ${credentials.username}
135
+ `);
136
+
137
+ // Set default privileges for future tables in schema
138
+ // Grant on tables created by the master user
139
+ await client.query(`
140
+ ALTER DEFAULT PRIVILEGES IN SCHEMA ${schemaName}
141
+ GRANT SELECT ON TABLES TO ${credentials.username}
142
+ `);
143
+
144
+ console.log(`✅ Configured access for schema: ${schemaName}`);
145
+ }
146
+
147
+ // Test the readonly user connection
148
+ console.log("🧪 Testing readonly user connection...");
149
+ const readonlyPool = new Pool({
150
+ host,
151
+ port,
152
+ user: credentials.username,
153
+ password: credentials.password,
154
+ database,
155
+ ssl: useSSL ? { rejectUnauthorized: false } : false,
156
+ });
157
+
158
+ try {
159
+ const readonlyClient = await readonlyPool.connect();
160
+ await readonlyClient.query("SELECT 1");
161
+ readonlyClient.release();
162
+ console.log("✅ Readonly user connection test successful");
163
+ } catch (error) {
164
+ console.error("⚠️ Readonly user connection test failed:", error);
165
+ return 1;
166
+ } finally {
167
+ await readonlyPool.end();
168
+ }
169
+
170
+ console.log("✅ Readonly user setup completed successfully");
171
+
172
+ // Return exit code 2 if user already existed (idempotent case)
173
+ // Return exit code 0 if user was created
174
+ return userCreated ? 0 : 2;
175
+ } catch (error) {
176
+ console.error("❌ Error setting up readonly user:", error);
177
+ return 1;
178
+ } finally {
179
+ if (client) {
180
+ client.release();
181
+ }
182
+ await masterPool.end();
183
+ }
184
+ }
185
+
186
+ async function main(): Promise<void> {
187
+ loadEnvConfig(process.cwd());
188
+
189
+ const exitCode = await setupReadonlyUser();
190
+ process.exit(exitCode);
191
+ }
192
+
193
+ void main();
@@ -0,0 +1,52 @@
1
+ # Check if database connection variables are set
2
+ if [ -z "$DATABASE_HOST" ] || [ -z "$DATABASE_USERNAME" ] || [ -z "$DATABASE_NAME" ]; then
3
+ echo "⚠️ Database connection not configured. Skipping migration."
4
+ echo "Required environment variables: DATABASE_HOST, DATABASE_USERNAME, DATABASE_NAME"
5
+ echo "❌ Error: Missing required database environment variables."
6
+ exit 1
7
+ else
8
+ echo "Database configuration found:"
9
+ echo " HOST: $DATABASE_HOST"
10
+ echo " USER: $DATABASE_USERNAME"
11
+ echo " DATABASE: $DATABASE_NAME"
12
+ echo " SSL: ${DATABASE_USE_SSL:-false}"
13
+ fi
14
+
15
+ # Run database migrations only if database is configured
16
+ echo "Running database migrations..."
17
+ if pnpm db:setup-and-migrate; then
18
+ echo "✅ Database migrations completed successfully"
19
+ else
20
+ echo "❌ Database migration failed. App will start anyway."
21
+ echo "Check your database configuration and connectivity."
22
+ fi
23
+
24
+ # Setup readonly database user for EDW (only if READONLY_SECRET_NAME is set)
25
+ if [ -n "$READONLY_SECRET_NAME" ]; then
26
+ echo "Setting up readonly database user for EDW..."
27
+ if pnpm db:setup-readonly; then
28
+ echo "✅ Readonly user setup completed"
29
+ else
30
+ EXIT_CODE=$?
31
+ if [ $EXIT_CODE -eq 2 ]; then
32
+ echo "ℹ️ Readonly user already exists and is configured correctly"
33
+ else
34
+ echo "⚠️ Failed to setup readonly user. Check logs for details."
35
+ fi
36
+ fi
37
+ else
38
+ echo "ℹ️ READONLY_SECRET_NAME not set, skipping readonly user setup"
39
+ fi
40
+
41
+ if [ -n "$ADMIN_USERNAME" ] && [ -n "$ADMIN_PASSWORD" ] && [ -n "$ADMIN_NAME" ]; then
42
+ echo "Creating admin user..."
43
+ if pnpm exec tsx scripts/create-user.ts "$ADMIN_USERNAME" "$ADMIN_PASSWORD" --name "$ADMIN_NAME"; then
44
+ echo "✅ Admin user created (or already exists)."
45
+ else
46
+ echo "⚠️ Failed to create admin user."
47
+ fi
48
+ fi
49
+
50
+ # Start the Next.js application
51
+ echo "Starting Next.js server on port ${PORT:-3000}..."
52
+ exec pnpm start
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+ import Header from "../../components/Header";
3
+
4
+ export default function AppLayout({
5
+ children,
6
+ }: {
7
+ children: React.ReactNode;
8
+ }) {
9
+ return (
10
+ <>
11
+ <Header />
12
+ <main>
13
+ <div className="py-8">
14
+ <div className="mx-auto max-w-6xl">
15
+ <div className="rounded-lg bg-white p-8">{children}</div>
16
+ </div>
17
+ </div>
18
+ </main>
19
+ </>
20
+ );
21
+ }
@@ -0,0 +1,30 @@
1
+ import type { Metadata } from "next";
2
+ import { headers } from "next/headers";
3
+ import { redirect } from "next/navigation";
4
+ import { auth } from "../../lib/auth";
5
+
6
+ export const metadata: Metadata = {
7
+ title: "__APP_TITLE__",
8
+ description: "__APP_TITLE__",
9
+ };
10
+
11
+ export default async function HomePage() {
12
+ const session = await auth.api.getSession({
13
+ headers: await headers(),
14
+ });
15
+
16
+ if (session?.user == null) {
17
+ redirect("/auth/signin");
18
+ }
19
+
20
+ return (
21
+ <div className="space-y-8">
22
+ <h1 className="text-center text-2xl font-bold text-foreground">
23
+ Welcome to __APP_TITLE__
24
+ </h1>
25
+ <p className="mx-auto max-w-xl text-center text-sm text-muted-foreground">
26
+ You are logged in. Start building your application!
27
+ </p>
28
+ </div>
29
+ );
30
+ }
@@ -0,0 +1,103 @@
1
+ "use client";
2
+
3
+ import { zodResolver } from "@hookform/resolvers/zod";
4
+ import { Button, Input } from "@percepta/design";
5
+ import { useRouter, useSearchParams } from "next/navigation";
6
+ import React, { useCallback } from "react";
7
+ import { Controller, useForm } from "react-hook-form";
8
+ import { toast } from "sonner";
9
+ import z from "zod";
10
+ import { FormItem } from "../../../../components/form/FormItem";
11
+ import { authClient } from "../../../../lib/auth-client";
12
+ import { IS_DEV } from "../../../../config/isDev";
13
+
14
+ const CREDENTIALS_SCHEMA = z.object({
15
+ email: z.string().min(1, "Email is required"),
16
+ password: z.string().min(1, "Password is required"),
17
+ });
18
+
19
+ type Credentials = z.infer<typeof CREDENTIALS_SCHEMA>;
20
+
21
+ // Defaults are empty in production so deployed instances don't suggest seeded
22
+ // credentials. In dev we prefill with the seeded user from `pnpm db:seed` so a
23
+ // fresh scaffold is one click away from authed.
24
+ const DEFAULTS: Credentials = IS_DEV
25
+ ? { email: "admin@example.com", password: "password" }
26
+ : { email: "", password: "" };
27
+
28
+ export function CredentialsSignInForm() {
29
+ const router = useRouter();
30
+ const searchParams = useSearchParams();
31
+
32
+ const {
33
+ control,
34
+ formState: { isSubmitting },
35
+ handleSubmit,
36
+ } = useForm<Credentials>({
37
+ resolver: zodResolver(CREDENTIALS_SCHEMA),
38
+ defaultValues: DEFAULTS,
39
+ });
40
+
41
+ const callbackUrl = searchParams.get("callbackUrl") ?? "/";
42
+
43
+ const submit = useCallback(
44
+ async ({ email, password }: Credentials): Promise<void> => {
45
+ const { error } = await authClient.signIn.email({
46
+ email,
47
+ password,
48
+ callbackURL: callbackUrl,
49
+ });
50
+
51
+ if (error != null) {
52
+ toast.error(
53
+ error.message ??
54
+ "Invalid email or password. Please check your credentials and try again.",
55
+ );
56
+ return;
57
+ }
58
+
59
+ router.push(callbackUrl);
60
+ router.refresh();
61
+ },
62
+ [callbackUrl, router],
63
+ );
64
+
65
+ return (
66
+ <div className="space-y-8">
67
+ <div className="text-center">
68
+ <h1 className="text-2xl font-bold text-foreground">Sign In</h1>
69
+ </div>
70
+ <form className="space-y-6" onSubmit={handleSubmit(submit)}>
71
+ <div className="space-y-4">
72
+ <Controller
73
+ control={control}
74
+ name="email"
75
+ render={({ field, fieldState }) => (
76
+ <FormItem label="Email" fieldState={fieldState}>
77
+ <Input {...field} type="email" autoComplete="email" />
78
+ </FormItem>
79
+ )}
80
+ />
81
+ <Controller
82
+ control={control}
83
+ name="password"
84
+ render={({ field, fieldState }) => (
85
+ <FormItem label="Password" fieldState={fieldState}>
86
+ <Input
87
+ {...field}
88
+ type="password"
89
+ autoComplete="current-password"
90
+ />
91
+ </FormItem>
92
+ )}
93
+ />
94
+ </div>
95
+ <div className="flex justify-end">
96
+ <Button type="submit" loading={isSubmitting}>
97
+ Sign In
98
+ </Button>
99
+ </div>
100
+ </form>
101
+ </div>
102
+ );
103
+ }
@@ -0,0 +1,30 @@
1
+ import type { Metadata } from "next";
2
+ import { headers } from "next/headers";
3
+ import { redirect } from "next/navigation";
4
+ import { Suspense } from "react";
5
+ import { auth } from "../../../../lib/auth";
6
+ import { CredentialsSignInForm } from "./CredentialsSignInForm";
7
+
8
+ export const metadata: Metadata = {
9
+ title: "Sign In — __APP_TITLE__",
10
+ };
11
+
12
+ export default async function SignInPage() {
13
+ const session = await auth.api.getSession({
14
+ headers: await headers(),
15
+ });
16
+
17
+ if (session?.user != null) {
18
+ redirect("/");
19
+ }
20
+
21
+ return (
22
+ <Suspense
23
+ fallback={
24
+ <p className="text-center text-sm text-muted-foreground">Loading…</p>
25
+ }
26
+ >
27
+ <CredentialsSignInForm />
28
+ </Suspense>
29
+ );
30
+ }
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+
3
+ export default function AuthLayout({
4
+ children,
5
+ }: {
6
+ children: React.ReactNode;
7
+ }) {
8
+ return (
9
+ <div className="flex min-h-screen flex-col items-center justify-center bg-muted/40 p-4">
10
+ <div className="w-full max-w-md rounded-lg border border-border bg-card p-8 shadow-sm">
11
+ {children}
12
+ </div>
13
+ </div>
14
+ );
15
+ }
@@ -0,0 +1,4 @@
1
+ import { toNextJsHandler } from "better-auth/next-js";
2
+ import { auth } from "../../../../lib/auth";
3
+
4
+ export const { GET, POST } = toNextJsHandler(auth);
@@ -0,0 +1,10 @@
1
+ import { checkStartup } from "../../../startup-checks";
2
+
3
+ export async function GET() {
4
+ const isHealthy = await checkStartup();
5
+ if (!isHealthy) {
6
+ return new Response(null, { status: 503 });
7
+ }
8
+
9
+ return new Response(null, { status: 200 });
10
+ }
@@ -0,0 +1,31 @@
1
+ import { serve } from "inngest/next";
2
+ import { compact } from "lodash-es";
3
+ import { type InngestFunctionCollection } from "../../../services/inngest/InngestFunctionCollection";
4
+ import { InngestService } from "../../../services/inngest/InngestService";
5
+
6
+ export const dynamic = "force-dynamic";
7
+
8
+ const functionCollections: InngestFunctionCollection[] = compact([]);
9
+
10
+ // InngestService.create() reads env vars and throws if INNGEST_BASE_URL is
11
+ // unset. Defer initialization until request time so `next build` can compile
12
+ // this route without those vars present.
13
+ type Handlers = ReturnType<typeof serve>;
14
+
15
+ let cachedHandlers: Handlers | undefined;
16
+
17
+ function getHandlers(): Handlers {
18
+ if (cachedHandlers == null) {
19
+ const inngestService = InngestService.create();
20
+ cachedHandlers = serve({
21
+ client: inngestService.client,
22
+ functions: functionCollections.flatMap(({ functions }) => functions),
23
+ signingKey: inngestService.signingKey,
24
+ });
25
+ }
26
+ return cachedHandlers;
27
+ }
28
+
29
+ export const GET: Handlers["GET"] = (...args) => getHandlers().GET(...args);
30
+ export const POST: Handlers["POST"] = (...args) => getHandlers().POST(...args);
31
+ export const PUT: Handlers["PUT"] = (...args) => getHandlers().PUT(...args);
@@ -0,0 +1,31 @@
1
+ import { getLogger } from "../../../services/logger/AppLogger";
2
+ import { withAppRouterRequestContext } from "../../../services/logger/withRequestContext";
3
+ import { checkStartup } from "../../../startup-checks";
4
+ import { syncInngestApp } from "../../../utils/syncInngestApp";
5
+
6
+ let hasSynced = false;
7
+
8
+ const handler = withAppRouterRequestContext(async (): Promise<Response> => {
9
+ const isHealthy = await checkStartup();
10
+ if (!isHealthy) {
11
+ return new Response(null, { status: 503 });
12
+ }
13
+
14
+ if (!hasSynced) {
15
+ try {
16
+ await syncInngestApp();
17
+ hasSynced = true;
18
+ } catch (error) {
19
+ getLogger().warn(
20
+ undefined,
21
+ "Failed to sync with Inngest Server during readiness",
22
+ error,
23
+ );
24
+ return new Response(null, { status: 503 });
25
+ }
26
+ }
27
+
28
+ return new Response(null, { status: 200 });
29
+ });
30
+
31
+ export { handler as GET };
@@ -0,0 +1,21 @@
1
+ import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
2
+ import { appRouter } from "../../../../server/api/root";
3
+ import { createContext } from "../../../../server/trpc";
4
+ import { getLogger } from "../../../../services/logger/AppLogger";
5
+ import { withAppRouterRequestContext } from "../../../../services/logger/withRequestContext";
6
+
7
+ const handler = withAppRouterRequestContext(
8
+ (req: Request): Promise<Response> => {
9
+ return fetchRequestHandler({
10
+ endpoint: "/api/trpc",
11
+ req,
12
+ router: appRouter,
13
+ createContext,
14
+ onError: ({ error }) => {
15
+ getLogger().error(undefined, "tRPC error.", error);
16
+ },
17
+ });
18
+ },
19
+ );
20
+
21
+ export { handler as GET, handler as POST };
@@ -0,0 +1,27 @@
1
+ "use client";
2
+
3
+ export default function GlobalError({
4
+ error,
5
+ reset,
6
+ }: {
7
+ error: Error & { digest?: string };
8
+ reset: () => void;
9
+ }) {
10
+ try {
11
+ const { faro } = require("@grafana/faro-web-sdk");
12
+ faro.api?.pushError(error);
13
+ } catch {
14
+ // Faro may not be initialized yet — don't let reporting break the error page
15
+ }
16
+
17
+ return (
18
+ <html lang="en">
19
+ <body suppressHydrationWarning>
20
+ <div style={{ padding: "2rem", textAlign: "center" }}>
21
+ <h1>Something went wrong</h1>
22
+ <button onClick={() => reset()}>Try again</button>
23
+ </div>
24
+ </body>
25
+ </html>
26
+ );
27
+ }
@@ -0,0 +1,18 @@
1
+ import "@percepta/design/styles";
2
+ import "../styles/globals.css";
3
+ import React from "react";
4
+ import { Providers } from "../components/Providers";
5
+
6
+ export default function RootLayout({
7
+ children,
8
+ }: {
9
+ children: React.ReactNode;
10
+ }) {
11
+ return (
12
+ <html lang="en">
13
+ <body className="antialiased" suppressHydrationWarning={true}>
14
+ <Providers>{children}</Providers>
15
+ </body>
16
+ </html>
17
+ );
18
+ }
@@ -0,0 +1,37 @@
1
+ "use client";
2
+
3
+ import { FaroProvider as BaseFaroProvider } from "@percepta/next-utils/faro";
4
+ import { Alert, AlertTitle, AlertDescription, Button } from "@percepta/design";
5
+ import { type ReactNode } from "react";
6
+
7
+ // Import to trigger Faro initialization at module scope (earliest possible)
8
+ import "../services/observability/initFaro";
9
+
10
+ export function AppFaroProvider({ children }: { children: ReactNode }) {
11
+ return (
12
+ <BaseFaroProvider fallback={<ErrorFallback />}>
13
+ {children}
14
+ </BaseFaroProvider>
15
+ );
16
+ }
17
+
18
+ function ErrorFallback() {
19
+ return (
20
+ <div className="flex min-h-screen items-center justify-center p-4">
21
+ <Alert variant="destructive" className="max-w-md">
22
+ <AlertTitle>Something went wrong</AlertTitle>
23
+ <AlertDescription>
24
+ An unexpected error occurred. Please refresh the page and try again.
25
+ </AlertDescription>
26
+ <Button
27
+ variant="outline"
28
+ size="sm"
29
+ className="mt-4"
30
+ onClick={() => window.location.reload()}
31
+ >
32
+ Refresh page
33
+ </Button>
34
+ </Alert>
35
+ </div>
36
+ );
37
+ }