@polishd/next 0.1.0 → 0.2.1-dev.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.
- package/AGENTS.md +161 -46
- package/CHANGELOG.md +163 -0
- package/README.md +275 -49
- package/bin/doctor.mjs +374 -0
- package/bin/init.mjs +215 -188
- package/bin/polishd.mjs +85 -0
- package/bin/shared.mjs +114 -0
- package/dist/ai/actions.d.ts +13 -2
- package/dist/ai/actions.d.ts.map +1 -1
- package/dist/ai/actions.js +19 -1
- package/dist/ai/actions.js.map +1 -1
- package/dist/ai/design.d.ts +28 -0
- package/dist/ai/design.d.ts.map +1 -0
- package/dist/ai/design.js +245 -0
- package/dist/ai/design.js.map +1 -0
- package/dist/ai/guard.d.ts +44 -3
- package/dist/ai/guard.d.ts.map +1 -1
- package/dist/ai/guard.js +13 -2
- package/dist/ai/guard.js.map +1 -1
- package/dist/ai/providers.d.ts +2 -0
- package/dist/ai/providers.d.ts.map +1 -1
- package/dist/ai/providers.js +70 -0
- package/dist/ai/providers.js.map +1 -1
- package/dist/ai/settings.d.ts +15 -1
- package/dist/ai/settings.d.ts.map +1 -1
- package/dist/ai/settings.js +38 -1
- package/dist/ai/settings.js.map +1 -1
- package/dist/ai/types.d.ts +57 -0
- package/dist/ai/types.d.ts.map +1 -1
- package/dist/client/design-scan.d.ts +37 -0
- package/dist/client/design-scan.d.ts.map +1 -0
- package/dist/client/design-scan.js +319 -0
- package/dist/client/design-scan.js.map +1 -0
- package/dist/client/init.d.ts.map +1 -1
- package/dist/client/init.js +67 -2
- package/dist/client/init.js.map +1 -1
- package/dist/config.d.ts +57 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +62 -3
- package/dist/config.js.map +1 -1
- package/dist/dashboard/design-review-card.d.ts +15 -0
- package/dist/dashboard/design-review-card.d.ts.map +1 -0
- package/dist/dashboard/design-review-card.js +82 -0
- package/dist/dashboard/design-review-card.js.map +1 -0
- package/dist/dashboard/design.d.ts +10 -0
- package/dist/dashboard/design.d.ts.map +1 -0
- package/dist/dashboard/design.js +83 -0
- package/dist/dashboard/design.js.map +1 -0
- package/dist/dashboard/index.d.ts +35 -5
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +176 -18
- package/dist/dashboard/index.js.map +1 -1
- package/dist/dashboard/journeys.d.ts.map +1 -1
- package/dist/dashboard/journeys.js +1 -0
- package/dist/dashboard/journeys.js.map +1 -1
- package/dist/dashboard/summary.d.ts.map +1 -1
- package/dist/dashboard/summary.js +59 -5
- package/dist/dashboard/summary.js.map +1 -1
- package/dist/dashboard/token-auth.d.ts +56 -0
- package/dist/dashboard/token-auth.d.ts.map +1 -0
- package/dist/dashboard/token-auth.js +158 -0
- package/dist/dashboard/token-auth.js.map +1 -0
- package/dist/dashboard/unlock.d.ts +2 -0
- package/dist/dashboard/unlock.d.ts.map +1 -0
- package/dist/dashboard/unlock.js +54 -0
- package/dist/dashboard/unlock.js.map +1 -0
- package/dist/dashboard.css +2740 -0
- package/dist/proxy.d.ts +53 -5
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +73 -3
- package/dist/proxy.js.map +1 -1
- package/dist/route.d.ts.map +1 -1
- package/dist/route.js +11 -3
- package/dist/route.js.map +1 -1
- package/dist/server/design.d.ts +93 -0
- package/dist/server/design.d.ts.map +1 -0
- package/dist/server/design.js +430 -0
- package/dist/server/design.js.map +1 -0
- package/dist/server/ingest.d.ts +33 -2
- package/dist/server/ingest.d.ts.map +1 -1
- package/dist/server/ingest.js +70 -8
- package/dist/server/ingest.js.map +1 -1
- package/dist/server/queries.d.ts +16 -0
- package/dist/server/queries.d.ts.map +1 -1
- package/dist/server/queries.js +80 -16
- package/dist/server/queries.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +4 -2
- package/dist/session.js.map +1 -1
- package/dist/shared/types.d.ts +31 -1
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js +1 -0
- package/dist/shared/types.js.map +1 -1
- package/docs/SETUP.md +308 -51
- package/package.json +14 -7
package/docs/SETUP.md
CHANGED
|
@@ -15,8 +15,6 @@ instead.
|
|
|
15
15
|
- **Node 22.5 or later.** The development store uses Node's built-in
|
|
16
16
|
`node:sqlite`. On older Node, Polishd degrades to a no-op rather than
|
|
17
17
|
crashing, but you won't capture anything locally.
|
|
18
|
-
- **Tailwind CSS** (v3 or v4) if you want the dashboard styled. See
|
|
19
|
-
[Styling](#4-styling) — this step is easy to miss.
|
|
20
18
|
- **Postgres** for production. Not needed for local development.
|
|
21
19
|
|
|
22
20
|
## 1. Install
|
|
@@ -31,21 +29,49 @@ Polishd at a Postgres URL. You don't need to install it yourself.
|
|
|
31
29
|
## 2. Scaffold
|
|
32
30
|
|
|
33
31
|
```bash
|
|
34
|
-
npx polishd init
|
|
32
|
+
npx @polishd/next init
|
|
35
33
|
```
|
|
36
34
|
|
|
37
|
-
This detects whether your app uses `src/` or the repo root,
|
|
38
|
-
|
|
35
|
+
This detects whether your app uses `src/` or the repo root, detects your
|
|
36
|
+
installed Next major so the proxy file gets the right name, writes four files,
|
|
37
|
+
and adds `.polishd/` to `.gitignore`. Useful flags:
|
|
39
38
|
|
|
40
39
|
| Flag | Effect |
|
|
41
40
|
|---|---|
|
|
42
41
|
| `--dry-run` | Print what would happen; write nothing |
|
|
43
|
-
| `--force` |
|
|
42
|
+
| `--force` | Regenerate files `init` itself wrote, backing each up to `.bak` |
|
|
44
43
|
| `--js` | Emit JavaScript instead of TypeScript |
|
|
45
44
|
| `--config` | Also write a starter `polishd.config.ts` |
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
**`--force` will never overwrite a file `init` didn't write.** Generated files
|
|
47
|
+
carry a marker; anything without one is left alone whether or not you pass the
|
|
48
|
+
flag, so re-running is safe even if you've since hand-written a proxy. When it
|
|
49
|
+
finds a proxy it didn't author, it prints merge instructions instead — see
|
|
50
|
+
[Composing with an existing proxy](#composing-with-an-existing-proxy).
|
|
51
|
+
|
|
52
|
+
Then check the result:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx @polishd/next doctor
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Installing with a coding agent
|
|
59
|
+
|
|
60
|
+
Paste this to Claude Code, Cursor, Copilot, or any other coding agent:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
Install @polishd/next into this Next.js app.
|
|
64
|
+
|
|
65
|
+
Follow the procedure in AGENTS.md from the package exactly — after installing,
|
|
66
|
+
read node_modules/@polishd/next/AGENTS.md. If the package isn't installed yet,
|
|
67
|
+
read it at https://github.com/muskanvaswan/polishd/blob/main/AGENTS.md
|
|
68
|
+
|
|
69
|
+
Work through its phases in order and do not skip Phase 7 (verification).
|
|
70
|
+
Several of its constraints fail silently at runtime rather than at build time,
|
|
71
|
+
so a passing build does not mean the install worked.
|
|
72
|
+
|
|
73
|
+
Finish by running `npx @polishd/next doctor` and fixing everything it reports.
|
|
74
|
+
```
|
|
49
75
|
|
|
50
76
|
## 3. What the four files do
|
|
51
77
|
|
|
@@ -81,8 +107,12 @@ export const config = {
|
|
|
81
107
|
> entire `/api` segment. The proxy's only job is setting a cookie, which page
|
|
82
108
|
> navigations already trigger.
|
|
83
109
|
|
|
84
|
-
|
|
85
|
-
|
|
110
|
+
### Composing with an existing proxy
|
|
111
|
+
|
|
112
|
+
**Already have a proxy or middleware?** Don't replace it — Next loads exactly
|
|
113
|
+
one such module, so Polishd has to go inside yours.
|
|
114
|
+
|
|
115
|
+
Threading the response is the easy half:
|
|
86
116
|
|
|
87
117
|
```ts
|
|
88
118
|
export function proxy(request: NextRequest) {
|
|
@@ -91,6 +121,42 @@ export function proxy(request: NextRequest) {
|
|
|
91
121
|
}
|
|
92
122
|
```
|
|
93
123
|
|
|
124
|
+
A redirect or rewrite you return keeps working and carries the analytics cookie.
|
|
125
|
+
|
|
126
|
+
The hard half is the matcher, and it's easy to miss. One file means one
|
|
127
|
+
`config.matcher`, so merging means widening it to the union of both — and
|
|
128
|
+
Polishd's matcher is near-total. A handler you deliberately scoped to two routes
|
|
129
|
+
now runs on **every request in the app**. Nothing errors. Your redirects just
|
|
130
|
+
start firing where they never did before.
|
|
131
|
+
|
|
132
|
+
`composePolishd` fixes that if you tell it the original scope:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { composePolishd } from "@polishd/next/proxy";
|
|
136
|
+
|
|
137
|
+
function myProxy(request: NextRequest) {
|
|
138
|
+
if (!hasSession(request)) return NextResponse.redirect(new URL("/login", request.url));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Still runs only on /admin/*, even though the matcher below is near-total.
|
|
142
|
+
export const proxy = composePolishd(myProxy, { matcher: "/admin/:path*" });
|
|
143
|
+
|
|
144
|
+
export const config = {
|
|
145
|
+
matcher: ["/((?!api|_next/static|_next/image|favicon|assets).*)"],
|
|
146
|
+
};
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Read your existing `config.matcher` before replacing it and pass exactly that
|
|
150
|
+
value. `matcher` takes Next's matcher strings (`"/admin/:path*"`,
|
|
151
|
+
`"/((?!api).*)"`) or a `RegExp` for full control. Omit it only if your handler
|
|
152
|
+
really did run everywhere.
|
|
153
|
+
|
|
154
|
+
Widening is safe from Polishd's side: `withPolishdSession` returns early once
|
|
155
|
+
the cookie exists, so the extra paths cost a cookie lookup and nothing more.
|
|
156
|
+
|
|
157
|
+
The union literal still has to be written by hand — Next parses it statically,
|
|
158
|
+
so no helper can generate it.
|
|
159
|
+
|
|
94
160
|
### `src/instrumentation-client.ts` — capture
|
|
95
161
|
|
|
96
162
|
Next runs this before hydration, so capture starts on the first paint.
|
|
@@ -125,6 +191,7 @@ export const POST = createPolishdRoute();
|
|
|
125
191
|
### `src/app/polishd/page.tsx` — the dashboard
|
|
126
192
|
|
|
127
193
|
```tsx
|
|
194
|
+
import "@polishd/next/dashboard.css";
|
|
128
195
|
import { createPolishdPage } from "@polishd/next/dashboard";
|
|
129
196
|
|
|
130
197
|
export const runtime = "nodejs";
|
|
@@ -133,44 +200,144 @@ export const dynamic = "force-dynamic";
|
|
|
133
200
|
export default createPolishdPage();
|
|
134
201
|
```
|
|
135
202
|
|
|
136
|
-
Same inline-config rule as the route.
|
|
203
|
+
Same inline-config rule as the route. In production this refuses to render
|
|
204
|
+
until you decide how it is protected — see
|
|
137
205
|
[Protecting the dashboard](#5-protecting-the-dashboard).
|
|
138
206
|
|
|
139
|
-
|
|
207
|
+
### The dashboard's own shell
|
|
140
208
|
|
|
141
|
-
The dashboard is
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
209
|
+
The dashboard is a route in *your* app, so it inherits your `<body>`. That
|
|
210
|
+
matters when your body doesn't scroll — the default shape of every dashboard
|
|
211
|
+
shell, chat UI, editor, and map app, all of which set `overflow: hidden` on
|
|
212
|
+
body and scroll their own inner regions.
|
|
145
213
|
|
|
146
|
-
|
|
214
|
+
By default Polishd renders into a self-contained shell (`position: fixed`, its
|
|
215
|
+
own background and scroll container) so host body styles can't strand content
|
|
216
|
+
below the fold. Opt out to inherit your page instead:
|
|
147
217
|
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
@source "../../node_modules/@polishd/next/dist";
|
|
218
|
+
```tsx
|
|
219
|
+
export default createPolishdPage({ shell: false });
|
|
151
220
|
```
|
|
152
221
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
this
|
|
222
|
+
**If the dashboard still looks cut off**, an ancestor is establishing a
|
|
223
|
+
containing block — `transform`, `filter`, `contain`, or `will-change` on
|
|
224
|
+
anything above the route defeats `position: fixed`. Rare this close to the
|
|
225
|
+
root. The complete escape is a second root layout via a route group, which
|
|
226
|
+
gives the dashboard its own `<html>`/`<body>` and total isolation from your
|
|
227
|
+
global CSS:
|
|
156
228
|
|
|
157
|
-
|
|
229
|
+
```
|
|
230
|
+
app/
|
|
231
|
+
(site)/layout.tsx ← your existing root layout, moved into a group
|
|
232
|
+
(polishd)/
|
|
233
|
+
layout.tsx ← renders its own <html> and <body>
|
|
234
|
+
polishd/page.tsx
|
|
235
|
+
```
|
|
158
236
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
237
|
+
This is the nuclear option, and it has real costs: you must move your own root
|
|
238
|
+
layout into a group, and navigation between the two roots forces a full page
|
|
239
|
+
reload. Reach for it only for a host with genuinely hostile global CSS.
|
|
240
|
+
|
|
241
|
+
## 4. Styling
|
|
242
|
+
|
|
243
|
+
The dashboard ships its own stylesheet, compiled at publish time. One import,
|
|
244
|
+
which `init` writes for you:
|
|
245
|
+
|
|
246
|
+
```tsx
|
|
247
|
+
// app/polishd/page.tsx
|
|
248
|
+
import "@polishd/next/dashboard.css";
|
|
164
249
|
```
|
|
165
250
|
|
|
166
|
-
**
|
|
167
|
-
|
|
168
|
-
|
|
251
|
+
**Tailwind is not required in your app.** The dashboard renders identically
|
|
252
|
+
whether or not you use it, and whichever version you use.
|
|
253
|
+
|
|
254
|
+
The stylesheet is built so it cannot interfere with the app it lands in, and so
|
|
255
|
+
the app cannot interfere with it:
|
|
256
|
+
|
|
257
|
+
- **No global preflight.** Tailwind's reset is the part that restyles every
|
|
258
|
+
element on a page, and it is deliberately left out. A scoped equivalent covers
|
|
259
|
+
the dashboard's own subtree instead, so your headings, lists, buttons and form
|
|
260
|
+
controls are untouched.
|
|
261
|
+
- **Every rule is scoped to `.polishd-root`**, checked at build time — no
|
|
262
|
+
selector in the file can match your markup.
|
|
263
|
+
- **The scope is written twice** (`.polishd-root.polishd-root`) so the dashboard
|
|
264
|
+
wins specificity ties. Tailwind class names are a global namespace: a host with
|
|
265
|
+
its own `.flex` or `.hidden` would otherwise style the dashboard's markup, and
|
|
266
|
+
which one won would depend on stylesheet order.
|
|
267
|
+
- **No cascade layers.** Unlayered CSS beats layered CSS outright regardless of
|
|
268
|
+
specificity, so a plain `h1 { color: … }` in your app would override the
|
|
269
|
+
dashboard's typography if its utilities stayed in `@layer utilities`.
|
|
270
|
+
- **An explicit font stack**, rather than inheriting yours — with no global
|
|
271
|
+
preflight, inheriting would land on the browser default and render the whole
|
|
272
|
+
dashboard in serif.
|
|
273
|
+
|
|
274
|
+
The one thing it cannot win is a host `!important` rule targeting the same
|
|
275
|
+
property. Nothing short of `!important` on our side would, and a package
|
|
276
|
+
shouting over its host is worse than a rare collision. If you hit one, the
|
|
277
|
+
route-group layout in [The dashboard's own shell](#the-dashboards-own-shell)
|
|
278
|
+
isolates the dashboard completely.
|
|
279
|
+
|
|
280
|
+
### Upgrading from 0.1.x
|
|
281
|
+
|
|
282
|
+
**No change is required.** Your existing `@source
|
|
283
|
+
"…/node_modules/@polishd/next/dist"` (or the `content` glob in
|
|
284
|
+
`tailwind.config.*`) still compiles the dashboard's utilities exactly as it did
|
|
285
|
+
in 0.1.x, so the dashboard stays styled after upgrading. Both paths were
|
|
286
|
+
rendered side by side and compared across 491 elements with no visible
|
|
287
|
+
difference.
|
|
288
|
+
|
|
289
|
+
Moving to the shipped stylesheet is recommended but optional, and **the order
|
|
290
|
+
matters**:
|
|
291
|
+
|
|
292
|
+
1. Add `import "@polishd/next/dashboard.css";` to your dashboard page.
|
|
293
|
+
2. Check the dashboard still looks right.
|
|
294
|
+
3. *Then* remove the `@source` line or `content` glob.
|
|
295
|
+
|
|
296
|
+
Removing the old wiring before adding the import is the one sequence that
|
|
297
|
+
leaves the dashboard unstyled, which is why the steps are in this order.
|
|
298
|
+
|
|
299
|
+
The payoff is that Tailwind stops being a requirement for your app at all, and
|
|
300
|
+
your build stops compiling utilities that only the dashboard uses.
|
|
301
|
+
`npx @polishd/next doctor` reports which of the two setups it finds and will not
|
|
302
|
+
report a working 0.1.x install as broken.
|
|
169
303
|
|
|
170
304
|
## 5. Protecting the dashboard
|
|
171
305
|
|
|
172
|
-
|
|
173
|
-
|
|
306
|
+
The dashboard is not a read-only report: its settings change the configured
|
|
307
|
+
model and **API base URL**, so whoever can reach it can point your requests at
|
|
308
|
+
their own endpoint and spend your key.
|
|
309
|
+
|
|
310
|
+
Locally it is open, with one console warning. **In production it refuses to
|
|
311
|
+
render** until you've chosen one of three options.
|
|
312
|
+
|
|
313
|
+
### Option 1 — the built-in token (no auth of your own required)
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
POLISHD_DASHBOARD_TOKEN=$(openssl rand -hex 32)
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
That's the whole setup. The locked screen shows a password field, and the token
|
|
320
|
+
is exchanged for an httpOnly, path-scoped cookie by a server action — so it
|
|
321
|
+
never appears in a URL, in browser history, in server logs, or in a `Referer`
|
|
322
|
+
header. The cookie carries an HMAC of the token rather than the token itself,
|
|
323
|
+
comparison is constant-time, expiry is enforced server-side, and the unlock form
|
|
324
|
+
is rate-limited per IP.
|
|
325
|
+
|
|
326
|
+
If your dashboard isn't at `/polishd`, say so, since that's what scopes the
|
|
327
|
+
cookie:
|
|
328
|
+
|
|
329
|
+
```tsx
|
|
330
|
+
import { createPolishdPage, polishdTokenAuth } from "@polishd/next/dashboard";
|
|
331
|
+
|
|
332
|
+
export default createPolishdPage({
|
|
333
|
+
authenticate: polishdTokenAuth({
|
|
334
|
+
basePath: "/admin/polishd",
|
|
335
|
+
maxAgeSeconds: 60 * 60 * 24, // default is one week
|
|
336
|
+
}),
|
|
337
|
+
});
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Option 2 — your app's own auth
|
|
174
341
|
|
|
175
342
|
```tsx
|
|
176
343
|
import { createPolishdPage } from "@polishd/next/dashboard";
|
|
@@ -186,25 +353,46 @@ export default createPolishdPage({
|
|
|
186
353
|
});
|
|
187
354
|
```
|
|
188
355
|
|
|
189
|
-
`authenticate` returns a boolean or a promise of one
|
|
190
|
-
`
|
|
191
|
-
one. Polishd ships no auth code, so it never pulls an auth library into your
|
|
192
|
-
dependency tree.
|
|
356
|
+
`authenticate` returns a boolean or a promise of one, and receives the request's
|
|
357
|
+
`cookies` and `headers`:
|
|
193
358
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
leave the actions callable by anyone — able to change the configured model and
|
|
198
|
-
API base URL, and spend your model tokens. The callback re-runs against the
|
|
199
|
-
caller's cookies on every action call.
|
|
359
|
+
```tsx
|
|
360
|
+
authenticate: (ctx) => ctx.cookies.get("admin_session")?.value === expected,
|
|
361
|
+
```
|
|
200
362
|
|
|
201
|
-
|
|
363
|
+
Taking them as arguments rather than calling `cookies()` inside means the
|
|
364
|
+
callback is testable without standing up a request. A zero-argument callback
|
|
365
|
+
still works unchanged. When it returns false the `unauthorized` node renders, or
|
|
366
|
+
a minimal built-in screen if you didn't pass one. Polishd ships no auth library,
|
|
367
|
+
so it never pulls one into your dependency tree.
|
|
202
368
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
369
|
+
### Option 3 — public on purpose
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
POLISHD_DASHBOARD_PUBLIC=true
|
|
206
373
|
```
|
|
207
374
|
|
|
375
|
+
An explicit opt-in, greppable in your deployment config. You'll still get one
|
|
376
|
+
warning per boot in production, because that's the only place it's actionable.
|
|
377
|
+
|
|
378
|
+
### Why this is stricter than it looks
|
|
379
|
+
|
|
380
|
+
**The gate covers the AI server actions, not just the page.** A Next server
|
|
381
|
+
action is an independently addressable POST endpoint whose id ships inside your
|
|
382
|
+
public client bundle. Gating only the page render would leave the actions
|
|
383
|
+
callable by anyone — able to change the configured model and API base URL, and
|
|
384
|
+
spend your model tokens. Your check re-runs against the caller's cookies on
|
|
385
|
+
every action call.
|
|
386
|
+
|
|
387
|
+
That's also why production fails shut at the *policy* level rather than merely
|
|
388
|
+
rendering a locked page: a page that showed a lock while still registering
|
|
389
|
+
"open" would leave every action reachable behind a locked-looking door.
|
|
390
|
+
|
|
391
|
+
One consequence worth knowing: if you delete `app/polishd/page.tsx` but keep the
|
|
392
|
+
package installed, the actions remain routable and always deny. That's correct,
|
|
393
|
+
but the symptom — actions failing with an opaque digest — is puzzling if you
|
|
394
|
+
meet it cold.
|
|
395
|
+
|
|
208
396
|
## 6. Verify it works
|
|
209
397
|
|
|
210
398
|
```bash
|
|
@@ -213,6 +401,20 @@ npm run build && npm start
|
|
|
213
401
|
|
|
214
402
|
Then, in order:
|
|
215
403
|
|
|
404
|
+
The fastest check is the doctor, which inspects the whole wiring and, with
|
|
405
|
+
`--url`, confirms the live path too:
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
npx @polishd/next doctor --url http://localhost:3000
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
It catches the failures that produce no error: a proxy named for the wrong Next
|
|
412
|
+
major, `config.matcher` that isn't an inline literal, route segment config
|
|
413
|
+
re-exported rather than declared, the dashboard stylesheet not imported, and events
|
|
414
|
+
being dropped for want of a session cookie.
|
|
415
|
+
|
|
416
|
+
By hand:
|
|
417
|
+
|
|
216
418
|
1. **Cookie.** `curl -sI http://localhost:3000/ | grep -i set-cookie` should
|
|
217
419
|
show `polishd_session=<uuid>` with `HttpOnly`.
|
|
218
420
|
2. **Dashboard.** Visit `/polishd` — expect HTTP 200, styled, with a "no events
|
|
@@ -273,9 +475,18 @@ export default definePolishdConfig({
|
|
|
273
475
|
sampleRate: 1, // fraction of sessions captured
|
|
274
476
|
rageClick: { count: 3, windowMs: 500 }, // N clicks on one element
|
|
275
477
|
flushIntervalMs: 10_000,
|
|
478
|
+
dashboardRoute: "/polishd", // never captured; see below
|
|
276
479
|
});
|
|
277
480
|
```
|
|
278
481
|
|
|
482
|
+
The dashboard's own traffic is **never captured**. Reading your analytics is
|
|
483
|
+
browsing too, and left alone it would rank "Refresh summary" as your site's
|
|
484
|
+
most-used feature. Nothing on `dashboardRoute` (or below it) is collected, and
|
|
485
|
+
anything captured before you upgraded is filtered out of the dashboard's
|
|
486
|
+
figures. Set `dashboardRoute` only if you mounted `createPolishdPage()`
|
|
487
|
+
somewhere other than `/polishd` — and pass the config to
|
|
488
|
+
`createPolishdRoute(config)` as well, so the server drops it too.
|
|
489
|
+
|
|
279
490
|
```ts
|
|
280
491
|
// src/instrumentation-client.ts
|
|
281
492
|
import { initPolishd } from "@polishd/next/client";
|
|
@@ -284,20 +495,66 @@ import config from "../polishd.config";
|
|
|
284
495
|
initPolishd(config);
|
|
285
496
|
```
|
|
286
497
|
|
|
287
|
-
|
|
288
|
-
|
|
498
|
+
> **This file is not loaded for you.** It can't be: the three places that read
|
|
499
|
+
> config run in three different runtimes — the browser, the Edge proxy, and the
|
|
500
|
+
> Node route handler — and the Edge one has no filesystem to read it from.
|
|
501
|
+
|
|
502
|
+
So pass the same object to all three:
|
|
503
|
+
|
|
504
|
+
```ts
|
|
505
|
+
initPolishd(config); // instrumentation-client.ts
|
|
506
|
+
export const POST = createPolishdRoute(config); // app/api/polishd/route.ts
|
|
507
|
+
const { proxy } = createPolishdProxy(config); // proxy.ts
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
Thread it into one but not another and they disagree silently. A proxy minting
|
|
511
|
+
cookie `a` while ingest reads `b` produces exactly the "beacons return 200,
|
|
512
|
+
dashboard stays empty" failure below.
|
|
513
|
+
|
|
514
|
+
For `sessionCookie` specifically, use the environment variable instead — it's
|
|
515
|
+
the one channel all three runtimes can read, so it can't go half-applied:
|
|
516
|
+
|
|
517
|
+
```bash
|
|
518
|
+
POLISHD_SESSION_COOKIE=my_session
|
|
519
|
+
```
|
|
289
520
|
|
|
290
521
|
## Troubleshooting
|
|
291
522
|
|
|
523
|
+
Try `npx @polishd/next doctor` first — it names most of what follows directly,
|
|
524
|
+
and `--url http://localhost:3000` checks the live path as well.
|
|
525
|
+
|
|
526
|
+
**Beacons return 200 but the dashboard stays empty**
|
|
527
|
+
The commonest failure, and the quietest. Ingest deliberately answers a missing
|
|
528
|
+
session cookie with `{"ok":true,"stored":0,"reason":"no_session"}` so beacons
|
|
529
|
+
never retry, and `sendBeacon` can't read a response anyway — so nothing in the
|
|
530
|
+
browser can tell you. The dashboard shows a banner when it detects this, and the
|
|
531
|
+
dev console warns on the first flush. Causes, in order of likelihood: the proxy
|
|
532
|
+
file is named for the wrong Next major; its matcher doesn't cover the pages
|
|
533
|
+
you're visiting; a merge with an existing proxy went wrong; or the proxy and the
|
|
534
|
+
route disagree about the cookie name (set `POLISHD_SESSION_COOKIE`).
|
|
535
|
+
|
|
292
536
|
**Build fails: "can't recognize the exported `runtime` field in route"**
|
|
293
537
|
Route segment config was re-exported. Declare `runtime` and `dynamic` inline in
|
|
294
538
|
the route and page modules.
|
|
295
539
|
|
|
540
|
+
**`ExperimentalWarning: SQLite is an experimental feature and might change at
|
|
541
|
+
any time`**
|
|
542
|
+
Expected and harmless. That's Node warning about its own built-in `node:sqlite`
|
|
543
|
+
module, which the dev store uses — it isn't a sign Polishd is unstable, and it
|
|
544
|
+
doesn't appear in production, where Postgres is used instead. Nothing to fix.
|
|
545
|
+
|
|
546
|
+
**The dashboard is cut off — content below the fold is unreachable**
|
|
547
|
+
Your `<body>` doesn't scroll (`overflow: hidden`), which is normal for app
|
|
548
|
+
shells. Polishd renders into its own fixed, scrolling container by default, so
|
|
549
|
+
this should be handled; if it persists, an ancestor with `transform`, `filter`,
|
|
550
|
+
`contain` or `will-change` is defeating `position: fixed`. See
|
|
551
|
+
[The dashboard's own shell](#the-dashboards-own-shell).
|
|
552
|
+
|
|
296
553
|
**Every `/api/*` route suddenly 404s**
|
|
297
554
|
Your proxy matcher is matching `/api`. Restore the `(?!api|...)` exclusion.
|
|
298
555
|
|
|
299
556
|
**The dashboard is unstyled**
|
|
300
|
-
|
|
557
|
+
The page is missing `import "@polishd/next/dashboard.css";`. See [Styling](#4-styling).
|
|
301
558
|
|
|
302
559
|
**`ERR_UNKNOWN_BUILTIN_MODULE: node:sqlite`**
|
|
303
560
|
Node is older than 22.5. Upgrade, or configure Postgres.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polishd/next",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"description": "Drop-in product analytics for Next.js
|
|
3
|
+
"version": "0.2.1-dev.0",
|
|
4
|
+
"description": "Drop-in product analytics for Next.js — capture real user behavioral signals (rage/dead clicks, scroll depth, errors, web vitals) and explore them on a built-in dashboard. What gets measured gets improved.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"publishConfig": {
|
|
@@ -28,15 +28,17 @@
|
|
|
28
28
|
"url": "https://github.com/muskanvaswan/polishd/issues"
|
|
29
29
|
},
|
|
30
30
|
"files": [
|
|
31
|
+
"bin",
|
|
31
32
|
"dist",
|
|
32
33
|
"docs",
|
|
33
34
|
"AGENTS.md",
|
|
34
35
|
"DATABASE.md",
|
|
35
36
|
"README.md",
|
|
36
|
-
"LICENSE"
|
|
37
|
+
"LICENSE",
|
|
38
|
+
"CHANGELOG.md"
|
|
37
39
|
],
|
|
38
40
|
"bin": {
|
|
39
|
-
"polishd": "./bin/
|
|
41
|
+
"polishd": "./bin/polishd.mjs"
|
|
40
42
|
},
|
|
41
43
|
"exports": {
|
|
42
44
|
".": {
|
|
@@ -63,13 +65,15 @@
|
|
|
63
65
|
"types": "./dist/dashboard/index.d.ts",
|
|
64
66
|
"default": "./dist/dashboard/index.js"
|
|
65
67
|
},
|
|
66
|
-
"./package.json": "./package.json"
|
|
68
|
+
"./package.json": "./package.json",
|
|
69
|
+
"./dashboard.css": "./dist/dashboard.css"
|
|
67
70
|
},
|
|
68
71
|
"scripts": {
|
|
69
|
-
"build": "tsc -p tsconfig.build.json",
|
|
72
|
+
"build": "tsc -p tsconfig.build.json && npm run build:css",
|
|
70
73
|
"typecheck": "tsc -p tsconfig.json",
|
|
71
74
|
"clean": "rm -rf dist",
|
|
72
|
-
"prepublishOnly": "npm run clean && npm run build"
|
|
75
|
+
"prepublishOnly": "npm run clean && npm run build",
|
|
76
|
+
"build:css": "node scripts/build-css.mjs"
|
|
73
77
|
},
|
|
74
78
|
"peerDependencies": {
|
|
75
79
|
"next": ">=15",
|
|
@@ -80,13 +84,16 @@
|
|
|
80
84
|
"pg": "^8.0.0"
|
|
81
85
|
},
|
|
82
86
|
"devDependencies": {
|
|
87
|
+
"@tailwindcss/cli": "^4.3.3",
|
|
83
88
|
"@types/node": "^20.14.8",
|
|
84
89
|
"@types/pg": "^8.11.0",
|
|
85
90
|
"@types/react": "^19",
|
|
86
91
|
"@types/react-dom": "^19",
|
|
87
92
|
"next": "^16",
|
|
93
|
+
"postcss": "^8.5.26",
|
|
88
94
|
"react": "^19",
|
|
89
95
|
"react-dom": "^19",
|
|
96
|
+
"tailwindcss": "^4.3.3",
|
|
90
97
|
"typescript": "^5.5.2"
|
|
91
98
|
}
|
|
92
99
|
}
|