@percepta/create 4.1.7 → 4.1.8
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.
- package/dist/{git-ops-BD7JNnal.js → git-ops-BNpQnEc1.js} +1 -1
- package/dist/{git-ops-BD7JNnal.js.map → git-ops-BNpQnEc1.js.map} +1 -1
- package/dist/{github-D3YOEl91.js → github-BOp8VQCY.js} +1 -1
- package/dist/{github-D3YOEl91.js.map → github-BOp8VQCY.js.map} +1 -1
- package/dist/index.js +107 -15
- package/dist/index.js.map +1 -1
- package/dist/{init-BD3EyyLO.js → init-CsuO_mu2.js} +2 -4
- package/dist/{init-BD3EyyLO.js.map → init-CsuO_mu2.js.map} +1 -1
- package/dist/{register-app-DZg-Pmtd.js → register-app-mNc1oYVK.js} +3 -5
- package/dist/{register-app-DZg-Pmtd.js.map → register-app-mNc1oYVK.js.map} +1 -1
- package/dist/{register-os-blueprint-Cgq1rXzQ.js → register-os-blueprint-Gdyn0pN1.js} +3 -4
- package/dist/{register-os-blueprint-Cgq1rXzQ.js.map → register-os-blueprint-Gdyn0pN1.js.map} +1 -1
- package/dist/{status-K6raTwwu.js → status-BrK9v1yb.js} +3 -3
- package/dist/{status-K6raTwwu.js.map → status-BrK9v1yb.js.map} +1 -1
- package/dist/{sync-Bi958-2W.js → sync-DC5DhIBT.js} +3 -3
- package/dist/{sync-Bi958-2W.js.map → sync-DC5DhIBT.js.map} +1 -1
- package/dist/{upstream-CAraZeSS.js → upstream-PNL6DGtl.js} +3 -3
- package/dist/{upstream-CAraZeSS.js.map → upstream-PNL6DGtl.js.map} +1 -1
- package/package.json +3 -3
- package/template-versions.json +2 -2
- package/templates/monorepo/auth/src/drizzle/migrations/meta/0000_snapshot.json +547 -0
- package/templates/monorepo/package.json.template +6 -6
- package/templates/monorepo/pnpm-workspace.yaml +1 -1
- package/templates/webapp/AGENTS.md +38 -16
- package/templates/webapp/README.md +56 -58
- package/templates/webapp/agent-skills/access-control.md +10 -9
- package/templates/webapp/agent-skills/database.md +10 -4
- package/templates/webapp/agent-skills/inngest.md +10 -8
- package/templates/webapp/agent-skills/langfuse.md +7 -5
- package/templates/webapp/agent-skills/oneshot.md +15 -13
- package/templates/webapp/package.json.template +5 -5
- package/templates/webapp/playwright.config.ts +1 -2
- package/templates/webapp/src/app/(app)/page.tsx +5 -3
- package/templates/webapp/src/app/(auth)/layout.tsx +2 -2
- package/templates/webapp/src/app/(settings)/settings/page.tsx +20 -21
- package/templates/webapp/src/components/FaroProvider.tsx +1 -2
- package/templates/webapp/src/components/Header.tsx +2 -8
- package/templates/webapp/src/components/form/FormItem.tsx +1 -1
- package/templates/webapp/src/drizzle/db.ts +3 -1
- package/templates/webapp/src/drizzle/schema/utils/jsonbFromZod.ts +1 -1
- package/templates/webapp/src/instrumentation.ts +3 -6
- package/templates/webapp/src/lib/auth-client.ts +3 -2
- package/templates/webapp/src/lib/trpc.ts +1 -1
- package/templates/webapp/src/server/trpc.ts +1 -1
- package/templates/webapp/src/services/DatabaseService.ts +1 -1
- package/templates/webapp/src/services/access/AppAccessControl.ts +1 -3
- package/templates/webapp/src/services/inngest/AppWorkflowService.ts +1 -1
- package/templates/webapp/src/styles/globals.css +13 -2
- package/dist/manifest-By1SgOjC.js +0 -59
- package/dist/manifest-By1SgOjC.js.map +0 -1
- package/dist/template-versions-CEIP9vhl.js +0 -35
- package/dist/template-versions-CEIP9vhl.js.map +0 -1
- package/dist/validate-dssldJAj.js +0 -14
- package/dist/validate-dssldJAj.js.map +0 -1
|
@@ -82,23 +82,23 @@ src/
|
|
|
82
82
|
|
|
83
83
|
## Available Scripts
|
|
84
84
|
|
|
85
|
-
| Script
|
|
86
|
-
|
|
87
|
-
| `pnpm dev`
|
|
88
|
-
| `pnpm build`
|
|
89
|
-
| `pnpm start`
|
|
90
|
-
| `pnpm typecheck`
|
|
91
|
-
| `pnpm access:validate`
|
|
92
|
-
| `pnpm access:apply-local` | Apply the merged customer access schema to local SpiceDB
|
|
93
|
-
| `pnpm auth:db:migrate`
|
|
94
|
-
| `pnpm inngest:dev`
|
|
95
|
-
| `pnpm db:generate`
|
|
96
|
-
| `pnpm db:migrate`
|
|
97
|
-
| `pnpm db:studio`
|
|
98
|
-
| `pnpm db:seed`
|
|
99
|
-
| `pnpm test:e2e:install`
|
|
100
|
-
| `pnpm test:e2e`
|
|
101
|
-
| `pnpm test:e2e:ui`
|
|
85
|
+
| Script | Description |
|
|
86
|
+
| ------------------------- | ---------------------------------------------------------- |
|
|
87
|
+
| `pnpm dev` | Start development server with Turbopack |
|
|
88
|
+
| `pnpm build` | Build for production |
|
|
89
|
+
| `pnpm start` | Start production server |
|
|
90
|
+
| `pnpm typecheck` | Type-check with `tsc` |
|
|
91
|
+
| `pnpm access:validate` | Validate the access manifest and schema |
|
|
92
|
+
| `pnpm access:apply-local` | Apply the merged customer access schema to local SpiceDB |
|
|
93
|
+
| `pnpm auth:db:migrate` | Run migrations for the shared customer auth database |
|
|
94
|
+
| `pnpm inngest:dev` | Start the local Inngest dev server for this app |
|
|
95
|
+
| `pnpm db:generate` | Generate Drizzle migrations |
|
|
96
|
+
| `pnpm db:migrate` | Run database migrations |
|
|
97
|
+
| `pnpm db:studio` | Run migrations, then open Drizzle Studio |
|
|
98
|
+
| `pnpm db:seed` | Seed default shared-auth dev users and local access grants |
|
|
99
|
+
| `pnpm test:e2e:install` | Install the Chromium browser used by Playwright |
|
|
100
|
+
| `pnpm test:e2e` | Run Playwright e2e tests after local setup |
|
|
101
|
+
| `pnpm test:e2e:ui` | Run Playwright e2e tests in UI mode after local setup |
|
|
102
102
|
|
|
103
103
|
## End-to-End Tests
|
|
104
104
|
|
|
@@ -136,15 +136,11 @@ const logger = getLogger();
|
|
|
136
136
|
// Unsafe data (PII) is redacted
|
|
137
137
|
logger.info(
|
|
138
138
|
{ safe: { requestId }, unsafe: { email } },
|
|
139
|
-
"User action completed"
|
|
139
|
+
"User action completed",
|
|
140
140
|
);
|
|
141
141
|
|
|
142
142
|
// Errors should be passed as the third parameter
|
|
143
|
-
logger.error(
|
|
144
|
-
{ safe: { documentId } },
|
|
145
|
-
"Processing failed",
|
|
146
|
-
error
|
|
147
|
-
);
|
|
143
|
+
logger.error({ safe: { documentId } }, "Processing failed", error);
|
|
148
144
|
```
|
|
149
145
|
|
|
150
146
|
## Authentication
|
|
@@ -165,6 +161,7 @@ database Secret. Local development can omit it and use the root-created local
|
|
|
165
161
|
`auth` database.
|
|
166
162
|
|
|
167
163
|
To create dev users:
|
|
164
|
+
|
|
168
165
|
```bash
|
|
169
166
|
pnpm db:seed
|
|
170
167
|
# Creates customer-admin@example.com / password as a customer admin
|
|
@@ -181,56 +178,57 @@ App permissions are authored in `src/access/schema.zed`; `src/access/access.mani
|
|
|
181
178
|
|
|
182
179
|
### Application
|
|
183
180
|
|
|
184
|
-
| Variable
|
|
185
|
-
|
|
186
|
-
| `NODE_ENV`
|
|
187
|
-
| `APP_BASE_URL`
|
|
188
|
-
| `DEPLOYMENT_ENVIRONMENT` | Deployment environment label for telemetry | `NODE_ENV`
|
|
181
|
+
| Variable | Description | Default |
|
|
182
|
+
| ------------------------ | ------------------------------------------ | ------------- |
|
|
183
|
+
| `NODE_ENV` | Environment mode | `development` |
|
|
184
|
+
| `APP_BASE_URL` | Base URL for the app | - |
|
|
185
|
+
| `DEPLOYMENT_ENVIRONMENT` | Deployment environment label for telemetry | `NODE_ENV` |
|
|
189
186
|
|
|
190
187
|
### App Database
|
|
191
188
|
|
|
192
|
-
| Variable
|
|
193
|
-
|
|
189
|
+
| Variable | Description | Default |
|
|
190
|
+
| -------------- | ----------------------------- | ------------------------------------------------------------- |
|
|
194
191
|
| `DATABASE_URL` | App PostgreSQL connection URL | `postgresql://postgres:postgres@localhost:5434/__DB_NAME__` |
|
|
195
192
|
|
|
196
193
|
### Shared Auth Database
|
|
197
194
|
|
|
198
|
-
| Variable
|
|
199
|
-
|
|
200
|
-
| `AUTH_DATABASE_URL` | Shared auth database URL from the monorepo auth Secret | -
|
|
195
|
+
| Variable | Description | Default |
|
|
196
|
+
| ------------------- | ------------------------------------------------------ | ------- |
|
|
197
|
+
| `AUTH_DATABASE_URL` | Shared auth database URL from the monorepo auth Secret | - |
|
|
201
198
|
|
|
202
199
|
### Security
|
|
203
200
|
|
|
204
|
-
| Variable
|
|
205
|
-
|
|
201
|
+
| Variable | Description |
|
|
202
|
+
| ----------------------- | ----------------------------------- |
|
|
206
203
|
| `ENCRYPTION_SECRET_KEY` | 32-character key for URL encryption |
|
|
207
204
|
|
|
208
205
|
Generate a secret key:
|
|
206
|
+
|
|
209
207
|
```bash
|
|
210
208
|
node -e "console.log(require('crypto').randomBytes(16).toString('hex'))"
|
|
211
209
|
```
|
|
212
210
|
|
|
213
211
|
### Access Control
|
|
214
212
|
|
|
215
|
-
| Variable
|
|
216
|
-
|
|
217
|
-
| `SPICEDB_ENDPOINT`
|
|
218
|
-
| `SPICEDB_PRESHARED_KEY` | SpiceDB preshared key
|
|
219
|
-
| `SPICEDB_INSECURE`
|
|
213
|
+
| Variable | Description | Default |
|
|
214
|
+
| ----------------------- | --------------------------------- | ------------------- |
|
|
215
|
+
| `SPICEDB_ENDPOINT` | SpiceDB gRPC endpoint | `localhost:50051` |
|
|
216
|
+
| `SPICEDB_PRESHARED_KEY` | SpiceDB preshared key | `dev-spicedb-token` |
|
|
217
|
+
| `SPICEDB_INSECURE` | Use insecure local gRPC transport | `true` |
|
|
220
218
|
|
|
221
219
|
### Inngest (Background Jobs)
|
|
222
220
|
|
|
223
|
-
| Variable
|
|
224
|
-
|
|
225
|
-
| `INNGEST_BASE_URL`
|
|
221
|
+
| Variable | Description |
|
|
222
|
+
| --------------------- | ------------------- |
|
|
223
|
+
| `INNGEST_BASE_URL` | Inngest server URL |
|
|
226
224
|
| `INNGEST_SIGNING_KEY` | Inngest signing key |
|
|
227
|
-
| `INNGEST_EVENT_KEY`
|
|
225
|
+
| `INNGEST_EVENT_KEY` | Inngest event key |
|
|
228
226
|
|
|
229
227
|
### Langfuse (LLM Observability)
|
|
230
228
|
|
|
231
|
-
| Variable
|
|
232
|
-
|
|
233
|
-
| `LANGFUSE_BASE_URL`
|
|
229
|
+
| Variable | Description |
|
|
230
|
+
| --------------------- | ------------------- |
|
|
231
|
+
| `LANGFUSE_BASE_URL` | Langfuse server URL |
|
|
234
232
|
| `LANGFUSE_PUBLIC_KEY` | Langfuse public key |
|
|
235
233
|
| `LANGFUSE_SECRET_KEY` | Langfuse secret key |
|
|
236
234
|
|
|
@@ -238,12 +236,12 @@ Set these values through the target deployment platform when Langfuse is enabled
|
|
|
238
236
|
|
|
239
237
|
### LLM Providers
|
|
240
238
|
|
|
241
|
-
| Variable
|
|
242
|
-
|
|
243
|
-
| `ANTHROPIC_API_KEY` | Anthropic API key
|
|
244
|
-
| `OPENAI_API_KEY`
|
|
245
|
-
| `LLM_PROVIDER`
|
|
246
|
-
| `LLM_MODEL`
|
|
239
|
+
| Variable | Description |
|
|
240
|
+
| ------------------- | --------------------------------------------------- |
|
|
241
|
+
| `ANTHROPIC_API_KEY` | Anthropic API key |
|
|
242
|
+
| `OPENAI_API_KEY` | OpenAI API key for local or non-demo deployments |
|
|
243
|
+
| `LLM_PROVIDER` | Optional provider override: `anthropic` or `openai` |
|
|
244
|
+
| `LLM_MODEL` | Optional model override |
|
|
247
245
|
|
|
248
246
|
Use `LLMService` for backend model calls:
|
|
249
247
|
|
|
@@ -261,11 +259,11 @@ For local development, `pnpm dev` also loads `~/.config/percepta/create.env` whe
|
|
|
261
259
|
|
|
262
260
|
### OpenTelemetry / LGTM
|
|
263
261
|
|
|
264
|
-
| Variable
|
|
265
|
-
|
|
266
|
-
| `OTEL_EXPORTER_OTLP_ENDPOINT`
|
|
267
|
-
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | Optional trace-specific OTLP endpoint
|
|
268
|
-
| `OTEL_TRACES_EXPORTER`
|
|
262
|
+
| Variable | Description |
|
|
263
|
+
| ------------------------------------ | -------------------------------------------- |
|
|
264
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Base OTLP HTTP collector endpoint |
|
|
265
|
+
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | Optional trace-specific OTLP endpoint |
|
|
266
|
+
| `OTEL_TRACES_EXPORTER` | Set to `none` to disable server trace export |
|
|
269
267
|
|
|
270
268
|
The generated app sets its OpenTelemetry service name and deployment
|
|
271
269
|
environment resource label internally. Configure the collector endpoint through
|
|
@@ -12,11 +12,11 @@ The customer monorepo has one shared identity layer and one shared SpiceDB deplo
|
|
|
12
12
|
|
|
13
13
|
There are three separate authorization layers:
|
|
14
14
|
|
|
15
|
-
| Layer
|
|
16
|
-
|
|
17
|
-
| Customer admins
|
|
18
|
-
| Default application roles | Customer Applications admin
|
|
19
|
-
| App roles/resources
|
|
15
|
+
| Layer | Owner | Stored as |
|
|
16
|
+
| ------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
17
|
+
| Customer admins | Customer bootstrap / Applications admin | `core/customer:main#admin@core/user:<users.id>` |
|
|
18
|
+
| Default application roles | Customer Applications admin | `core/application:<app>#admin/user@core/user:<users.id>` or `core/group:<groups.id>#member` |
|
|
19
|
+
| App roles/resources | This app's admin UI and app code | `<app>/system:main#<role>@<subject>` and app resource relationships |
|
|
20
20
|
|
|
21
21
|
Customer admins do not automatically get application access. They can manage default app roles, but to enter the app as a user, a user or group must be an application `admin` or `user`.
|
|
22
22
|
|
|
@@ -195,9 +195,7 @@ const [canReadSalary] = await getAccessControl().permissions.canMany(checks);
|
|
|
195
195
|
Use a lifecycle sync wrapper in repositories/services that create, update, or delete rows with SpiceDB-backed relationships. Configure it once with either direct SpiceDB writes for authorization changes that must be visible immediately, or outbox enqueueing for transactional retry.
|
|
196
196
|
|
|
197
197
|
```ts
|
|
198
|
-
import {
|
|
199
|
-
createPermissionedResourceLifecycleSync,
|
|
200
|
-
} from "@percepta/access-control/drizzle";
|
|
198
|
+
import { createPermissionedResourceLifecycleSync } from "@percepta/access-control/drizzle";
|
|
201
199
|
|
|
202
200
|
const employeeLifecycle = createPermissionedResourceLifecycleSync({
|
|
203
201
|
apply: {
|
|
@@ -222,7 +220,10 @@ Do not model ordinary Postgres columns as SpiceDB objects. Model business permis
|
|
|
222
220
|
Use the app access service instead of constructing raw SpiceDB clients in feature code:
|
|
223
221
|
|
|
224
222
|
```ts
|
|
225
|
-
import {
|
|
223
|
+
import {
|
|
224
|
+
getAccessControl,
|
|
225
|
+
toUserSubject,
|
|
226
|
+
} from "@/services/access/AppAccessControl";
|
|
226
227
|
|
|
227
228
|
const allowed = await getAccessControl().permissions.can({
|
|
228
229
|
permission: "view_private",
|
|
@@ -60,13 +60,19 @@ import { eq } from "drizzle-orm";
|
|
|
60
60
|
const db = DatabaseService.create().getDatabase();
|
|
61
61
|
|
|
62
62
|
// Select
|
|
63
|
-
const docs = await db
|
|
63
|
+
const docs = await db
|
|
64
|
+
.select()
|
|
65
|
+
.from(documents)
|
|
66
|
+
.where(eq(documents.userId, userId));
|
|
64
67
|
|
|
65
68
|
// Insert
|
|
66
69
|
await db.insert(documents).values({ title: "New Doc", userId });
|
|
67
70
|
|
|
68
71
|
// Update
|
|
69
|
-
await db
|
|
72
|
+
await db
|
|
73
|
+
.update(documents)
|
|
74
|
+
.set({ title: "Updated" })
|
|
75
|
+
.where(eq(documents.id, docId));
|
|
70
76
|
|
|
71
77
|
// Delete
|
|
72
78
|
await db.delete(documents).where(eq(documents.id, docId));
|
|
@@ -129,8 +135,8 @@ pnpm --dir ../.. run docker:down
|
|
|
129
135
|
|
|
130
136
|
## Environment Variables
|
|
131
137
|
|
|
132
|
-
| Variable
|
|
133
|
-
|
|
138
|
+
| Variable | Default | Description |
|
|
139
|
+
| -------------- | ------------------------------------------------------------- | ----------------------------- |
|
|
134
140
|
| `DATABASE_URL` | `postgresql://postgres:postgres@localhost:5434/__DB_NAME__` | App PostgreSQL connection URL |
|
|
135
141
|
|
|
136
142
|
Shared auth data belongs to the customer monorepo auth package. Deployed apps
|
|
@@ -13,7 +13,9 @@ Create a Zod schema for the event payload:
|
|
|
13
13
|
```typescript
|
|
14
14
|
import z from "zod";
|
|
15
15
|
|
|
16
|
-
export type DocumentProcessedPayload = z.infer<
|
|
16
|
+
export type DocumentProcessedPayload = z.infer<
|
|
17
|
+
typeof DocumentProcessedPayload.SCHEMA
|
|
18
|
+
>;
|
|
17
19
|
export namespace DocumentProcessedPayload {
|
|
18
20
|
export const SCHEMA = z.object({
|
|
19
21
|
documentId: z.string(),
|
|
@@ -135,13 +137,13 @@ The Inngest Dev Server auto-discovers functions by calling the serve endpoint at
|
|
|
135
137
|
|
|
136
138
|
## Environment Variables
|
|
137
139
|
|
|
138
|
-
| Variable
|
|
139
|
-
|
|
140
|
-
| `INNGEST_BASE_URL`
|
|
141
|
-
| `INNGEST_EVENT_KEY`
|
|
142
|
-
| `INNGEST_SIGNING_KEY` | Yes (prod) | Function registration signing key
|
|
143
|
-
| `INNGEST_APP_URL`
|
|
144
|
-
| `SKIP_INNGEST_SYNC`
|
|
140
|
+
| Variable | Required | Description |
|
|
141
|
+
| --------------------- | ---------- | ---------------------------------------------------- |
|
|
142
|
+
| `INNGEST_BASE_URL` | Yes | Inngest server URL (`http://localhost:8288` locally) |
|
|
143
|
+
| `INNGEST_EVENT_KEY` | Yes (prod) | Event authentication key |
|
|
144
|
+
| `INNGEST_SIGNING_KEY` | Yes (prod) | Function registration signing key |
|
|
145
|
+
| `INNGEST_APP_URL` | No | Override app URL for Inngest to call back |
|
|
146
|
+
| `SKIP_INNGEST_SYNC` | No | Set `true` to skip Inngest app sync on startup |
|
|
145
147
|
|
|
146
148
|
## Key Concepts
|
|
147
149
|
|
|
@@ -7,6 +7,7 @@ Langfuse is an open-source LLM observability platform. It captures traces, spans
|
|
|
7
7
|
## When to Use Langfuse
|
|
8
8
|
|
|
9
9
|
**Use Langfuse when the app calls LLMs** (OpenAI, Bedrock/Claude, etc.). It gives you:
|
|
10
|
+
|
|
10
11
|
- Trace visualization of multi-step LLM chains
|
|
11
12
|
- Token usage and cost tracking per request
|
|
12
13
|
- Prompt versioning and A/B testing
|
|
@@ -69,6 +70,7 @@ platform's environment-scoped variable or secret mechanism.
|
|
|
69
70
|
### Self-Hosted / Langfuse Cloud
|
|
70
71
|
|
|
71
72
|
For external projects, you can:
|
|
73
|
+
|
|
72
74
|
- Use [Langfuse Cloud](https://cloud.langfuse.com) (free tier available)
|
|
73
75
|
- Self-host with `docker run langfuse/langfuse`
|
|
74
76
|
|
|
@@ -109,10 +111,10 @@ Simply don't set the `LANGFUSE_*` env vars. This is the default local developmen
|
|
|
109
111
|
|
|
110
112
|
## Environment Variables
|
|
111
113
|
|
|
112
|
-
| Variable
|
|
113
|
-
|
|
114
|
-
| `LANGFUSE_BASE_URL`
|
|
115
|
-
| `LANGFUSE_PUBLIC_KEY` | No
|
|
116
|
-
| `LANGFUSE_SECRET_KEY` | No
|
|
114
|
+
| Variable | Required | Description |
|
|
115
|
+
| --------------------- | -------- | ---------------------------------------------------------------- |
|
|
116
|
+
| `LANGFUSE_BASE_URL` | No | Langfuse server URL. If unset, Langfuse integration is disabled. |
|
|
117
|
+
| `LANGFUSE_PUBLIC_KEY` | No | Public key from Langfuse dashboard |
|
|
118
|
+
| `LANGFUSE_SECRET_KEY` | No | Secret key from Langfuse dashboard |
|
|
117
119
|
|
|
118
120
|
All three must be set for Langfuse to activate. If any is missing, the integration silently no-ops.
|
|
@@ -74,6 +74,7 @@ This is the ONLY point where you pause for user confirmation. After they confirm
|
|
|
74
74
|
Based on the confirmed requirements, design the architecture. Do NOT write a plan document — think through it and write it as a brief message to the user (so they can see your thinking), then start building.
|
|
75
75
|
|
|
76
76
|
Cover:
|
|
77
|
+
|
|
77
78
|
1. **Database schema** — which tables, columns, relationships
|
|
78
79
|
2. **API routes** — which tRPC routers and procedures
|
|
79
80
|
3. **Pages** — which routes/pages in the app
|
|
@@ -162,6 +163,7 @@ pnpm dev
|
|
|
162
163
|
### Step 3: Verify the app works
|
|
163
164
|
|
|
164
165
|
Open the app in a browser and walk through the core workflows from the requirements. Check:
|
|
166
|
+
|
|
165
167
|
- Pages load without errors
|
|
166
168
|
- Data can be created, read, updated, deleted
|
|
167
169
|
- Auth works (if configured)
|
|
@@ -215,16 +217,16 @@ Report the deployment URL to the user when done.
|
|
|
215
217
|
|
|
216
218
|
## Common Mistakes
|
|
217
219
|
|
|
218
|
-
| Mistake
|
|
219
|
-
|
|
220
|
-
| Stopping after design to ask permission
|
|
221
|
-
| Not running `pnpm build` between chunks
|
|
222
|
-
| Writing custom UI instead of using `@percepta/design`
|
|
223
|
-
| Using `console.log`
|
|
224
|
-
| Using `process.env`
|
|
225
|
-
| Forgetting to export new schema from the schema index
|
|
226
|
-
| Forgetting to register Inngest functions in the serve endpoint | Function collections must be added to the serve endpoint.
|
|
227
|
-
| Forgetting to add new router to the root appRouter
|
|
228
|
-
| Not verifying the app in the browser
|
|
229
|
-
| Creating the GitHub repo before the app works locally
|
|
230
|
-
| Deploying without the user asking
|
|
220
|
+
| Mistake | Fix |
|
|
221
|
+
| -------------------------------------------------------------- | ------------------------------------------------------------------ |
|
|
222
|
+
| Stopping after design to ask permission | After requirements are confirmed, build autonomously. |
|
|
223
|
+
| Not running `pnpm build` between chunks | Build after every chunk. Fix errors immediately. |
|
|
224
|
+
| Writing custom UI instead of using `@percepta/design` | Check the design system first. |
|
|
225
|
+
| Using `console.log` | Use `getLogger()` with safe/unsafe fields. |
|
|
226
|
+
| Using `process.env` | Use `getEnvConfig()`. |
|
|
227
|
+
| Forgetting to export new schema from the schema index | Every new table must be exported from the index. |
|
|
228
|
+
| Forgetting to register Inngest functions in the serve endpoint | Function collections must be added to the serve endpoint. |
|
|
229
|
+
| Forgetting to add new router to the root appRouter | Every new tRPC router must be composed in the root. |
|
|
230
|
+
| Not verifying the app in the browser | Build + lint passing is necessary but not sufficient. Test the UI. |
|
|
231
|
+
| Creating the GitHub repo before the app works locally | Verify locally first, then create the repo. |
|
|
232
|
+
| Deploying without the user asking | Only deploy when explicitly requested. |
|
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"engines": {
|
|
7
|
-
"node": ">=18.0.0"
|
|
8
|
-
},
|
|
9
6
|
"scripts": {
|
|
10
7
|
"dev": "tsx ./scripts/with-local-env.ts next dev --turbopack",
|
|
11
8
|
"build": "next build",
|
|
@@ -46,9 +43,9 @@
|
|
|
46
43
|
"@opentelemetry/auto-instrumentations-node": "^0.75.0",
|
|
47
44
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.217.0",
|
|
48
45
|
"@opentelemetry/sdk-node": "^0.217.0",
|
|
49
|
-
"@__CUSTOMER_SLUG__/auth": "workspace:*",
|
|
50
46
|
"@percepta/access-control": "^1.0.0",
|
|
51
47
|
"@percepta/ai": "^0.1.0",
|
|
48
|
+
"@__CUSTOMER_SLUG__/auth": "workspace:*",
|
|
52
49
|
"@percepta/database": "^0.1.2",
|
|
53
50
|
"@percepta/design": "^0.4.1",
|
|
54
51
|
"@percepta/inngest": "^0.1.0",
|
|
@@ -93,8 +90,8 @@
|
|
|
93
90
|
"zod": "^4.1.5"
|
|
94
91
|
},
|
|
95
92
|
"devDependencies": {
|
|
96
|
-
"@playwright/test": "^1.58.2",
|
|
97
93
|
"@percepta/build": "^1.0.0",
|
|
94
|
+
"@playwright/test": "^1.58.2",
|
|
98
95
|
"@tailwindcss/postcss": "^4.1.11",
|
|
99
96
|
"@types/formidable": "^3.4.5",
|
|
100
97
|
"@types/he": "^1.2.3",
|
|
@@ -112,5 +109,8 @@
|
|
|
112
109
|
"tailwindcss": "^4.0.12",
|
|
113
110
|
"vitest": "^4.0.0",
|
|
114
111
|
"yargs": "^17.7.2"
|
|
112
|
+
},
|
|
113
|
+
"engines": {
|
|
114
|
+
"node": ">=18.0.0"
|
|
115
115
|
}
|
|
116
116
|
}
|
|
@@ -2,8 +2,7 @@ import { defineConfig, devices } from "@playwright/test";
|
|
|
2
2
|
|
|
3
3
|
const port = Number(process.env.PLAYWRIGHT_PORT ?? 3000);
|
|
4
4
|
const host = process.env.PLAYWRIGHT_HOST ?? "localhost";
|
|
5
|
-
const baseURL =
|
|
6
|
-
process.env.PLAYWRIGHT_BASE_URL ?? `http://${host}:${port}`;
|
|
5
|
+
const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? `http://${host}:${port}`;
|
|
7
6
|
|
|
8
7
|
export default defineConfig({
|
|
9
8
|
testDir: "./e2e",
|
|
@@ -59,8 +59,8 @@ export default function HomePage() {
|
|
|
59
59
|
<p className="app-kicker">Overview</p>
|
|
60
60
|
<h1 className="app-title">Good afternoon, App Admin</h1>
|
|
61
61
|
<p className="app-subtitle">
|
|
62
|
-
Welcome to __APP_TITLE__. Three items want attention; the workspace
|
|
63
|
-
|
|
62
|
+
Welcome to __APP_TITLE__. Three items want attention; the workspace is
|
|
63
|
+
ready for your team's first workflows.
|
|
64
64
|
</p>
|
|
65
65
|
</div>
|
|
66
66
|
<div className="app-actions">
|
|
@@ -77,7 +77,9 @@ export default function HomePage() {
|
|
|
77
77
|
<span className="app-metric-label">{metric.label}</span>
|
|
78
78
|
<span
|
|
79
79
|
className={
|
|
80
|
-
metric.tone === "warn"
|
|
80
|
+
metric.tone === "warn"
|
|
81
|
+
? "app-chip app-chip-warn"
|
|
82
|
+
: "app-chip"
|
|
81
83
|
}
|
|
82
84
|
>
|
|
83
85
|
{metric.delta}
|
|
@@ -24,8 +24,8 @@ export default function AuthLayout({
|
|
|
24
24
|
A quieter place to do your team's most considered work.
|
|
25
25
|
</h2>
|
|
26
26
|
<p className="app-auth-copy">
|
|
27
|
-
Keep decisions, handoffs, and daily operations in one calm
|
|
28
|
-
|
|
27
|
+
Keep decisions, handoffs, and daily operations in one calm workspace
|
|
28
|
+
built for focused teams.
|
|
29
29
|
</p>
|
|
30
30
|
</div>
|
|
31
31
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
AccessRoleDefinition,
|
|
3
|
-
ApplicationGrant,
|
|
4
|
-
SubjectRef,
|
|
1
|
+
import {
|
|
2
|
+
type AccessRoleDefinition,
|
|
3
|
+
type ApplicationGrant,
|
|
4
|
+
type SubjectRef,
|
|
5
|
+
groupSubjectRef,
|
|
5
6
|
} from "@percepta/access-control";
|
|
6
|
-
import { groupSubjectRef } from "@percepta/access-control";
|
|
7
7
|
import {
|
|
8
8
|
PrincipalMultiInput,
|
|
9
9
|
type PrincipalOption,
|
|
@@ -32,6 +32,10 @@ import {
|
|
|
32
32
|
getCustomerAccessControl,
|
|
33
33
|
toUserSubject,
|
|
34
34
|
} from "../../../services/access/AppAccessControl";
|
|
35
|
+
import {
|
|
36
|
+
AccessControlTabs,
|
|
37
|
+
type AccessControlTab,
|
|
38
|
+
} from "./_components/AccessControlTabs";
|
|
35
39
|
import {
|
|
36
40
|
type AccessAppRole,
|
|
37
41
|
type SettingsPermissions,
|
|
@@ -39,10 +43,6 @@ import {
|
|
|
39
43
|
readAssignableRole,
|
|
40
44
|
requireAnySettingsPermission,
|
|
41
45
|
} from "./_lib/accessSettings";
|
|
42
|
-
import {
|
|
43
|
-
AccessControlTabs,
|
|
44
|
-
type AccessControlTab,
|
|
45
|
-
} from "./_components/AccessControlTabs";
|
|
46
46
|
|
|
47
47
|
export const metadata: Metadata = {
|
|
48
48
|
title: "Settings - __APP_TITLE__",
|
|
@@ -73,10 +73,10 @@ interface RoleAssignmentRow {
|
|
|
73
73
|
readonly definition: RoleDefinition;
|
|
74
74
|
readonly disabled: boolean;
|
|
75
75
|
readonly disabledReason?: string;
|
|
76
|
-
readonly hiddenFields?:
|
|
76
|
+
readonly hiddenFields?: ReadonlyArray<{
|
|
77
77
|
readonly name: string;
|
|
78
78
|
readonly value: string;
|
|
79
|
-
}
|
|
79
|
+
}>;
|
|
80
80
|
readonly options: readonly PrincipalAssignmentOption[];
|
|
81
81
|
readonly selectedSubjects: readonly SubjectRef[];
|
|
82
82
|
readonly updateAction: (formData: FormData) => Promise<void>;
|
|
@@ -123,7 +123,7 @@ const roleDefinitions = [
|
|
|
123
123
|
source: "Application access",
|
|
124
124
|
},
|
|
125
125
|
...(
|
|
126
|
-
accessRoleDefinitions as
|
|
126
|
+
accessRoleDefinitions as ReadonlyArray<AccessRoleDefinition<AccessAppRole>>
|
|
127
127
|
).map(
|
|
128
128
|
(definition): RoleDefinition => ({
|
|
129
129
|
...definition,
|
|
@@ -139,7 +139,9 @@ const appRoleDefinitionMap = new Map<AccessAppRole, RoleDefinition>(
|
|
|
139
139
|
.map((definition) => [definition.role as AccessAppRole, definition]),
|
|
140
140
|
);
|
|
141
141
|
|
|
142
|
-
export default async function SettingsPage({
|
|
142
|
+
export default async function SettingsPage({
|
|
143
|
+
searchParams,
|
|
144
|
+
}: SettingsPageProps) {
|
|
143
145
|
const permissions = await requireAnySettingsPermission();
|
|
144
146
|
const params = await searchParams;
|
|
145
147
|
const section = readSettingsSection(params?.section);
|
|
@@ -211,10 +213,7 @@ function RolesTab() {
|
|
|
211
213
|
</TableHeader>
|
|
212
214
|
<TableBody>
|
|
213
215
|
{roleDefinitions.map((role) => (
|
|
214
|
-
<TableRow
|
|
215
|
-
key={`${role.source}:${role.role}`}
|
|
216
|
-
className="align-top"
|
|
217
|
-
>
|
|
216
|
+
<TableRow key={`${role.source}:${role.role}`} className="align-top">
|
|
218
217
|
<TableCell className="py-4 whitespace-normal">
|
|
219
218
|
<div className="font-medium text-foreground">{role.label}</div>
|
|
220
219
|
<div className="text-muted-foreground">{role.description}</div>
|
|
@@ -492,10 +491,10 @@ async function listAssignableRoleSubjects(): Promise<
|
|
|
492
491
|
> {
|
|
493
492
|
const app = getAccessControl().app;
|
|
494
493
|
const entries = await Promise.all(
|
|
495
|
-
assignableRoles.map(
|
|
496
|
-
role
|
|
497
|
-
|
|
498
|
-
|
|
494
|
+
assignableRoles.map(
|
|
495
|
+
async (role) =>
|
|
496
|
+
[role, new Set(await app.listAppRoleSubjects(role))] as const,
|
|
497
|
+
),
|
|
499
498
|
);
|
|
500
499
|
|
|
501
500
|
return new Map(entries);
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { Alert, AlertDescription, AlertTitle, Button } from "@percepta/design";
|
|
4
4
|
import { FaroProvider as BaseFaroProvider } from "@percepta/next-utils/faro";
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import type { ReactNode } from "react";
|
|
7
6
|
// Import to trigger Faro initialization at module scope (earliest possible)
|
|
8
7
|
import "../services/observability/initFaro";
|
|
9
8
|
|
|
@@ -40,10 +40,7 @@ export default function Header({
|
|
|
40
40
|
return (
|
|
41
41
|
<header className="app-header">
|
|
42
42
|
<nav aria-label="Primary navigation" className="app-header-nav">
|
|
43
|
-
<Link
|
|
44
|
-
className="app-header-brand"
|
|
45
|
-
href="/"
|
|
46
|
-
>
|
|
43
|
+
<Link className="app-header-brand" href="/">
|
|
47
44
|
<span className="app-header-mark" aria-hidden={true}>
|
|
48
45
|
{appInitial}
|
|
49
46
|
</span>
|
|
@@ -66,10 +63,7 @@ export default function Header({
|
|
|
66
63
|
</div>
|
|
67
64
|
<DropdownMenu>
|
|
68
65
|
<DropdownMenuTrigger asChild={true}>
|
|
69
|
-
<button
|
|
70
|
-
className="app-account-button"
|
|
71
|
-
aria-label="Open account menu"
|
|
72
|
-
>
|
|
66
|
+
<button className="app-account-button" aria-label="Open account menu">
|
|
73
67
|
<div className="app-account-text">
|
|
74
68
|
<p className="text-sm font-medium text-foreground">
|
|
75
69
|
{user.name || "User"}
|
|
@@ -2,7 +2,7 @@ import { Label } from "@percepta/design";
|
|
|
2
2
|
import { Slot } from "@radix-ui/react-slot";
|
|
3
3
|
import { compact } from "lodash-es";
|
|
4
4
|
import React, { useId } from "react";
|
|
5
|
-
import {
|
|
5
|
+
import type { ControllerFieldState } from "react-hook-form";
|
|
6
6
|
import { cn } from "../../utils/cn";
|
|
7
7
|
|
|
8
8
|
interface FormItemProps extends React.ComponentProps<"div"> {
|
|
@@ -8,7 +8,9 @@ export const { client, db } = createDb();
|
|
|
8
8
|
function createDb(): { client: Pool; db: NodePgDatabase } {
|
|
9
9
|
const { DATABASE_URL: databaseUrl, NODE_ENV: nodeEnv } = getEnvConfig();
|
|
10
10
|
const pool = createPgPool(
|
|
11
|
-
readDatabaseConfig({
|
|
11
|
+
readDatabaseConfig({
|
|
12
|
+
env: { DATABASE_URL: databaseUrl, NODE_ENV: nodeEnv },
|
|
13
|
+
}),
|
|
12
14
|
);
|
|
13
15
|
|
|
14
16
|
return { client: pool, db: drizzle(pool) };
|
|
@@ -9,14 +9,11 @@ import { getLogger } from "./services/logger/AppLogger";
|
|
|
9
9
|
type SpanProcessor = tracing.SpanProcessor;
|
|
10
10
|
|
|
11
11
|
function setDefaultOpenTelemetryEnv(): void {
|
|
12
|
-
const {
|
|
13
|
-
|
|
14
|
-
NODE_ENV: nodeEnv,
|
|
15
|
-
} = getEnvConfig();
|
|
12
|
+
const { DEPLOYMENT_ENVIRONMENT: deploymentEnvironment, NODE_ENV: nodeEnv } =
|
|
13
|
+
getEnvConfig();
|
|
16
14
|
|
|
17
15
|
process.env.OTEL_SERVICE_NAME ??= "__APP_NAME__";
|
|
18
|
-
process.env.OTEL_RESOURCE_ATTRIBUTES ??=
|
|
19
|
-
`deployment.environment=${deploymentEnvironment ?? nodeEnv}`;
|
|
16
|
+
process.env.OTEL_RESOURCE_ATTRIBUTES ??= `deployment.environment=${deploymentEnvironment ?? nodeEnv}`;
|
|
20
17
|
}
|
|
21
18
|
|
|
22
19
|
function getOtlpTracesEndpoint(): string | undefined {
|