@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.
Files changed (95) hide show
  1. package/AGENTS.md +161 -46
  2. package/CHANGELOG.md +163 -0
  3. package/README.md +275 -49
  4. package/bin/doctor.mjs +374 -0
  5. package/bin/init.mjs +215 -188
  6. package/bin/polishd.mjs +85 -0
  7. package/bin/shared.mjs +114 -0
  8. package/dist/ai/actions.d.ts +13 -2
  9. package/dist/ai/actions.d.ts.map +1 -1
  10. package/dist/ai/actions.js +19 -1
  11. package/dist/ai/actions.js.map +1 -1
  12. package/dist/ai/design.d.ts +28 -0
  13. package/dist/ai/design.d.ts.map +1 -0
  14. package/dist/ai/design.js +245 -0
  15. package/dist/ai/design.js.map +1 -0
  16. package/dist/ai/guard.d.ts +44 -3
  17. package/dist/ai/guard.d.ts.map +1 -1
  18. package/dist/ai/guard.js +13 -2
  19. package/dist/ai/guard.js.map +1 -1
  20. package/dist/ai/providers.d.ts +2 -0
  21. package/dist/ai/providers.d.ts.map +1 -1
  22. package/dist/ai/providers.js +70 -0
  23. package/dist/ai/providers.js.map +1 -1
  24. package/dist/ai/settings.d.ts +15 -1
  25. package/dist/ai/settings.d.ts.map +1 -1
  26. package/dist/ai/settings.js +38 -1
  27. package/dist/ai/settings.js.map +1 -1
  28. package/dist/ai/types.d.ts +57 -0
  29. package/dist/ai/types.d.ts.map +1 -1
  30. package/dist/client/design-scan.d.ts +37 -0
  31. package/dist/client/design-scan.d.ts.map +1 -0
  32. package/dist/client/design-scan.js +319 -0
  33. package/dist/client/design-scan.js.map +1 -0
  34. package/dist/client/init.d.ts.map +1 -1
  35. package/dist/client/init.js +67 -2
  36. package/dist/client/init.js.map +1 -1
  37. package/dist/config.d.ts +57 -3
  38. package/dist/config.d.ts.map +1 -1
  39. package/dist/config.js +62 -3
  40. package/dist/config.js.map +1 -1
  41. package/dist/dashboard/design-review-card.d.ts +15 -0
  42. package/dist/dashboard/design-review-card.d.ts.map +1 -0
  43. package/dist/dashboard/design-review-card.js +82 -0
  44. package/dist/dashboard/design-review-card.js.map +1 -0
  45. package/dist/dashboard/design.d.ts +10 -0
  46. package/dist/dashboard/design.d.ts.map +1 -0
  47. package/dist/dashboard/design.js +83 -0
  48. package/dist/dashboard/design.js.map +1 -0
  49. package/dist/dashboard/index.d.ts +35 -5
  50. package/dist/dashboard/index.d.ts.map +1 -1
  51. package/dist/dashboard/index.js +176 -18
  52. package/dist/dashboard/index.js.map +1 -1
  53. package/dist/dashboard/journeys.d.ts.map +1 -1
  54. package/dist/dashboard/journeys.js +1 -0
  55. package/dist/dashboard/journeys.js.map +1 -1
  56. package/dist/dashboard/summary.d.ts.map +1 -1
  57. package/dist/dashboard/summary.js +59 -5
  58. package/dist/dashboard/summary.js.map +1 -1
  59. package/dist/dashboard/token-auth.d.ts +56 -0
  60. package/dist/dashboard/token-auth.d.ts.map +1 -0
  61. package/dist/dashboard/token-auth.js +158 -0
  62. package/dist/dashboard/token-auth.js.map +1 -0
  63. package/dist/dashboard/unlock.d.ts +2 -0
  64. package/dist/dashboard/unlock.d.ts.map +1 -0
  65. package/dist/dashboard/unlock.js +54 -0
  66. package/dist/dashboard/unlock.js.map +1 -0
  67. package/dist/dashboard.css +2740 -0
  68. package/dist/proxy.d.ts +53 -5
  69. package/dist/proxy.d.ts.map +1 -1
  70. package/dist/proxy.js +73 -3
  71. package/dist/proxy.js.map +1 -1
  72. package/dist/route.d.ts.map +1 -1
  73. package/dist/route.js +11 -3
  74. package/dist/route.js.map +1 -1
  75. package/dist/server/design.d.ts +93 -0
  76. package/dist/server/design.d.ts.map +1 -0
  77. package/dist/server/design.js +430 -0
  78. package/dist/server/design.js.map +1 -0
  79. package/dist/server/ingest.d.ts +33 -2
  80. package/dist/server/ingest.d.ts.map +1 -1
  81. package/dist/server/ingest.js +70 -8
  82. package/dist/server/ingest.js.map +1 -1
  83. package/dist/server/queries.d.ts +16 -0
  84. package/dist/server/queries.d.ts.map +1 -1
  85. package/dist/server/queries.js +80 -16
  86. package/dist/server/queries.js.map +1 -1
  87. package/dist/session.d.ts.map +1 -1
  88. package/dist/session.js +4 -2
  89. package/dist/session.js.map +1 -1
  90. package/dist/shared/types.d.ts +31 -1
  91. package/dist/shared/types.d.ts.map +1 -1
  92. package/dist/shared/types.js +1 -0
  93. package/dist/shared/types.js.map +1 -1
  94. package/docs/SETUP.md +308 -51
  95. package/package.json +14 -7
package/AGENTS.md CHANGED
@@ -18,13 +18,19 @@ Read these from the target app before changing anything.
18
18
  | Next.js App Router | an `app/` or `src/app/` directory exists | Stop. Polishd has no Pages Router support. |
19
19
  | Next.js ≥ 15 | `next` version in `package.json` | Stop and report. |
20
20
  | Node ≥ 22.5 | `node -v`, plus `engines`/`.nvmrc`/host setting | Capture silently no-ops on older Node. Warn the user and continue only if they accept that. |
21
- | Tailwind CSS | `tailwindcss` in dependencies | The dashboard renders unstyled. See Phase 4. |
22
21
  | Package manager | lockfile name | Use it consistently; don't mix. |
23
22
 
24
23
  Record two facts you will need throughout:
25
24
 
26
25
  - **`BASE`** — `src` if `src/app/` exists, otherwise `` (repo root).
27
- - **`NEXT_MAJOR`** — 16+ uses `proxy.ts`; 15 uses `middleware.ts`.
26
+ - **`NEXT_MAJOR`** — 16+ uses `proxy.ts`; 15 uses `middleware.ts`. Read the
27
+ *installed* version from `node_modules/next/package.json`, not the range in
28
+ `package.json` — `^16`, `latest` and `canary` tell you nothing about what is
29
+ actually installed. The CLI does this for you; you need it only for Phase 3b.
30
+
31
+ Getting `NEXT_MAJOR` wrong is a silent total failure, not a build error: Next
32
+ simply never loads a file under the other name, so no session cookie is minted,
33
+ every beacon still returns 200, and nothing is ever stored.
28
34
 
29
35
  ## Phase 2 — Install
30
36
 
@@ -37,18 +43,25 @@ Postgres URL is configured. No separate install is needed.
37
43
 
38
44
  ## Phase 3 — Scaffold
39
45
 
40
- Prefer the CLI. It handles every branch in Phase 1 and Phase 4 for you:
46
+ Prefer the CLI. It detects `BASE` and `NEXT_MAJOR` itself, so you do not have
47
+ to branch on them:
41
48
 
42
49
  ```bash
43
- npx polishd init
50
+ npx @polishd/next init
44
51
  ```
45
52
 
46
- Flags: `--dry-run` (preview), `--force` (overwrite existing), `--js` (JavaScript
47
- output), `--config` (also emit `polishd.config.ts`).
53
+ Flags: `--dry-run` (preview), `--force` (regenerate files the CLI itself wrote,
54
+ backing each up to `.bak` first), `--js` (JavaScript output), `--config` (also
55
+ emit `polishd.config.ts`).
56
+
57
+ It writes four files and appends `.polishd/` to `.gitignore`.
58
+ It names the proxy file for the detected Next major and reports which it chose;
59
+ if it could not determine the version it says so — check that against Phase 1
60
+ and rename by hand if it guessed wrong.
48
61
 
49
- It writes four files, appends `.polishd/` to `.gitignore`, and wires Tailwind.
50
- If it reports that a file already exists, do not overwrite blindly — merge by
51
- hand following Phase 3b.
62
+ **`--force` will not overwrite a file the CLI did not write**, so it is safe to
63
+ re-run. When it finds a proxy it did not author it prints merge instructions
64
+ instead. Follow Phase 3c, not Phase 3b, in that case.
52
65
 
53
66
  ### Phase 3b — Manual scaffold (only if the CLI could not run)
54
67
 
@@ -70,9 +83,7 @@ export const config = {
70
83
  };
71
84
  ```
72
85
 
73
- If the app already has a proxy/middleware, do **not** replace it. Call
74
- `withPolishdSession(request, existingResponse)` in the existing handler and
75
- merge the matcher, keeping the `api` exclusion.
86
+ If the app already has a proxy/middleware, do **not** replace it — see Phase 3c.
76
87
 
77
88
  **2. `{BASE}/instrumentation-client.ts`** — starts browser capture.
78
89
 
@@ -96,6 +107,7 @@ export const POST = createPolishdRoute();
96
107
  **4. `{BASE}/app/polishd/page.tsx`** — the dashboard.
97
108
 
98
109
  ```tsx
110
+ import "@polishd/next/dashboard.css";
99
111
  import { createPolishdPage } from "@polishd/next/dashboard";
100
112
 
101
113
  export const runtime = "nodejs";
@@ -106,29 +118,62 @@ export default createPolishdPage();
106
118
 
107
119
  Then add `.polishd/` to `.gitignore`.
108
120
 
109
- ## Phase 4 — Wire Tailwind
121
+ ### Phase 3c — The app already has a proxy or middleware
122
+
123
+ Next loads exactly one proxy module, so Polishd goes **inside** the existing
124
+ one. Never replace it: that file typically carries auth redirects, i18n, A/B
125
+ routing, or rate limiting.
126
+
127
+ The trap is the matcher. One file means one `config.matcher`, so merging means
128
+ widening it to the union — and Polishd's matcher is near-total. A handler that
129
+ was scoped to two routes silently begins running on every request in the app.
130
+ Nothing errors; its redirects just start firing where they never did before.
131
+
132
+ Use `composePolishd` and declare the handler's original scope, which is
133
+ re-applied as an internal guard:
134
+
135
+ ```ts
136
+ import { composePolishd } from "@polishd/next/proxy";
110
137
 
111
- The dashboard is built from Tailwind utility classes and ships **no stylesheet**.
112
- Tailwind will not find it on its own — v4's content detection deliberately
113
- ignores `node_modules`, and v3 only scans the globs you give it. Skipping this
114
- step produces a dashboard that returns HTTP 200 and is completely unstyled.
138
+ // Existing handler, unchanged.
139
+ function myProxy(request: NextRequest) { /* … */ }
115
140
 
116
- **Tailwind v4** (no `tailwind.config.*`; CSS has `@import "tailwindcss"`) — add
117
- to that CSS file, with the path relative to the file itself:
141
+ // Keeps running only on /admin/*, though the matcher below is near-total.
142
+ export const proxy = composePolishd(myProxy, { matcher: "/admin/:path*" });
118
143
 
119
- ```css
120
- @import "tailwindcss";
121
- @source "../../node_modules/@polishd/next/dist";
144
+ export const config = {
145
+ matcher: ["/((?!api|_next/static|_next/image|favicon|assets).*)"],
146
+ };
122
147
  ```
123
148
 
124
- **Tailwind v3** — add to `content` in `tailwind.config.*`:
149
+ Read the existing `config.matcher` **before** you overwrite it and pass exactly
150
+ that value as `matcher`. On Next 15 the file is `middleware.ts` and the export
151
+ must be named `middleware`.
125
152
 
126
- ```ts
127
- content: ["./src/**/*.{ts,tsx}", "./node_modules/@polishd/next/dist/**/*.js"],
153
+ `matcher` accepts Next's matcher strings or a `RegExp`. The union literal itself
154
+ must still be written by hand — Next statically parses it (Invariant 1).
155
+
156
+ ## Phase 4 — Styling
157
+
158
+ One import in the dashboard page, which the CLI writes for you:
159
+
160
+ ```tsx
161
+ import "@polishd/next/dashboard.css";
128
162
  ```
129
163
 
130
- **No Tailwind** — tell the user the dashboard will be unstyled. Do not add
131
- Tailwind to their app to fix this unless they ask.
164
+ The stylesheet is compiled at publish time and self-contained. **Do not add
165
+ Tailwind to the app, and do not add an `@source` line** — neither is needed, and
166
+ the host's Tailwind version is irrelevant to how the dashboard renders.
167
+
168
+ It carries no global preflight and every rule is scoped to `.polishd-root`, so
169
+ it cannot restyle the host app.
170
+
171
+ **On an existing 0.1.x install**, a `@source ".../@polishd/next/dist"` line in
172
+ the host's Tailwind config is still doing the styling and the dashboard is not
173
+ broken. Do not remove it as a first step. Add the stylesheet import, verify the
174
+ dashboard still renders correctly, and only then remove the `@source` line —
175
+ removing it first is the one order that leaves the dashboard unstyled. If the
176
+ app has no Tailwind at all, the import is the only thing that styles it.
132
177
 
133
178
  ## Phase 5 — Invariants
134
179
 
@@ -145,10 +190,13 @@ style preferences.
145
190
  fails the build with *"Next.js can't recognize the exported `runtime` field
146
191
  in route. It mustn't be reexported."*
147
192
 
148
- 3. **Gate the dashboard in production.** With no `authenticate` callback, the
149
- dashboard *and its server actions* are public: anyone reaching the URL can
150
- read the analytics, change the configured model and API base URL, and spend
151
- the owner's model tokens. See Phase 6.
193
+ 3. **The dashboard refuses to serve in production until access is decided.**
194
+ It is not a read-only report: its settings change the configured model and
195
+ API base URL, so reaching it means being able to redirect the owner's
196
+ requests and spend their key. With no `POLISHD_DASHBOARD_TOKEN`, no
197
+ `authenticate` callback, and no `POLISHD_DASHBOARD_PUBLIC=true`, production
198
+ renders a locked screen and every server action denies. Development is
199
+ unaffected. See Phase 6.
152
200
 
153
201
  4. **Never import `@polishd/next/server` from a client component.** It pulls
154
202
  database drivers into the browser bundle. Use `@polishd/next/client` there.
@@ -158,8 +206,32 @@ style preferences.
158
206
 
159
207
  ## Phase 6 — Protect the dashboard
160
208
 
161
- If the app has any auth helper, wire it up. Ask the user which one rather than
162
- guessing.
209
+ Pick one of three. **If the app has no auth of its own, use the token** — do not
210
+ hand-roll a gate, and do not leave it for the user to figure out later.
211
+
212
+ **1. Built-in token (default choice for an app with no auth).** No code:
213
+
214
+ ```bash
215
+ POLISHD_DASHBOARD_TOKEN=$(openssl rand -hex 32)
216
+ ```
217
+
218
+ Add it to the app's `.env.example` and tell the user to set it in their
219
+ deployment environment. The locked screen renders a password form; the token is
220
+ exchanged for an httpOnly, path-scoped cookie by a server action, so it never
221
+ travels in a URL. Constant-time comparison, HMAC in the cookie rather than the
222
+ raw token, server-side expiry, and per-IP rate limiting are all handled.
223
+
224
+ Pass options only if the dashboard is not at `/polishd`:
225
+
226
+ ```tsx
227
+ import { createPolishdPage, polishdTokenAuth } from "@polishd/next/dashboard";
228
+ export default createPolishdPage({
229
+ authenticate: polishdTokenAuth({ basePath: "/admin/polishd" }),
230
+ });
231
+ ```
232
+
233
+ **2. The app's own auth.** If the app has an auth helper, prefer it. Ask the
234
+ user which one rather than guessing.
163
235
 
164
236
  ```tsx
165
237
  import { createPolishdPage } from "@polishd/next/dashboard";
@@ -170,25 +242,50 @@ export const runtime = "nodejs";
170
242
  export const dynamic = "force-dynamic";
171
243
 
172
244
  export default createPolishdPage({
173
- authenticate: isAuthenticated, // () => boolean | Promise<boolean>
245
+ authenticate: isAuthenticated, // (ctx) => boolean | Promise<boolean>
174
246
  unauthorized: <MyLogin />, // optional; a minimal screen otherwise
175
247
  });
176
248
  ```
177
249
 
178
- The same callback guards the AI server actions, re-running per request against
179
- the caller's cookies. Polishd ships no auth code of its own.
250
+ `authenticate` receives `{ cookies, headers }` for the request, so it need not
251
+ call `cookies()`/`headers()` itself. A zero-argument callback still works.
180
252
 
181
- A common pattern is to stay open in development and require auth in production:
253
+ **3. Deliberately public.** Only if the user asks for it:
182
254
 
183
- ```tsx
184
- authenticate: async () =>
185
- process.env.NODE_ENV !== "production" || (await isAuthenticated()),
255
+ ```bash
256
+ POLISHD_DASHBOARD_PUBLIC=true
186
257
  ```
187
258
 
259
+ In every case the same policy guards the AI server actions, re-running per
260
+ request against the caller's cookies. Note that an app which deletes
261
+ `app/polishd/page.tsx` while keeping the package leaves those actions routable
262
+ but always denying — correct, but confusing if you meet it cold.
263
+
188
264
  ## Phase 7 — Verify
189
265
 
190
266
  Run all of these. Report the results; do not declare success from the build
191
- alone.
267
+ alone — most of what can be wrong here builds cleanly and fails silently.
268
+
269
+ Start with the doctor, which checks the whole wiring in one command:
270
+
271
+ ```bash
272
+ npx @polishd/next doctor
273
+ ```
274
+
275
+ It verifies the proxy is named and exported for the detected Next major, that
276
+ `config.matcher` is an inline literal excluding `/api`, that `runtime` and
277
+ `dynamic` are declared rather than re-exported, that capture and ingest exist,
278
+ that the dashboard stylesheet is imported, and the access posture. Fix everything it
279
+ reports before continuing.
280
+
281
+ With the app running, add live checks — this is the only way to confirm the
282
+ cookie is really minted and events really store:
283
+
284
+ ```bash
285
+ npx @polishd/next doctor --url http://localhost:3000
286
+ ```
287
+
288
+ Then confirm by hand:
192
289
 
193
290
  ```bash
194
291
  # 1. Build must pass.
@@ -213,7 +310,7 @@ curl -s -X POST http://localhost:3000/api/polishd \
213
310
  # expect: {"ok":true,"stored":1}
214
311
 
215
312
  # 6. Styling is present — the stylesheet must contain dashboard utilities.
216
- # Fetch the dashboard's CSS chunk and grep for `tabular-nums`.
313
+ # The dashboard page must import "@polishd/next/dashboard.css".
217
314
  ```
218
315
 
219
316
  In development the store writes `.polishd/analytics.db` (SQLite). Its presence
@@ -228,6 +325,9 @@ survive there.
228
325
  | Variable | Required | Purpose |
229
326
  |---|---|---|
230
327
  | `POLISHD_DATABASE_URL` | for production capture | Pooled Postgres connection string |
328
+ | `POLISHD_DASHBOARD_TOKEN` | for production, unless auth is wired | Protects `/polishd`. `openssl rand -hex 32` |
329
+ | `POLISHD_DASHBOARD_PUBLIC` | no | `true` to serve the dashboard unguarded on purpose |
330
+ | `POLISHD_SESSION_COOKIE` | no | Rename the session cookie; read by both proxy and ingest |
231
331
  | `POLISHD_DB_PATH` | no | Custom SQLite path in dev |
232
332
  | `POLISHD_AI_*` | no | Preset AI provider/model/key instead of using the dashboard |
233
333
  | `POLISHD_GITHUB_*` | no | Repo + token for filing issues from findings |
@@ -240,14 +340,22 @@ a notice on the dashboard, and no impact on the host app.
240
340
 
241
341
  ## Troubleshooting
242
342
 
343
+ Run `npx @polishd/next doctor` first — it names most of these directly.
344
+
243
345
  | Symptom | Cause | Fix |
244
346
  |---|---|---|
245
347
  | *"can't recognize the exported `runtime` field"* | Route segment config re-exported | Declare `runtime`/`dynamic` inline (Phase 3b) |
246
348
  | Every `/api/*` route 404s | Proxy matcher matches `/api` | Restore the `api` exclusion in the matcher |
247
- | Dashboard renders unstyled | Tailwind isn't scanning `dist` | Phase 4 |
349
+ | Dashboard renders unstyled | `@polishd/next/dashboard.css` not imported in the page | Phase 4 |
248
350
  | Dashboard is empty, warning about the store | No writable backend | Set `POLISHD_DATABASE_URL`, or check Node ≥ 22.5 in dev |
351
+ | Dashboard shows *"no session cookie — your proxy isn't running"* | Proxy missing, misnamed for the Next major, matcher not covering the path, or a botched merge | Phase 3c and `doctor --url` |
352
+ | Beacons return 200 but nothing is stored | Same as above — ingest answers `{ok:true,stored:0,reason:"no_session"}` so the client cannot see a failure | `doctor --url http://localhost:3000` |
353
+ | Proxy and ingest disagree on the cookie name | `sessionCookie` threaded into one layer but not another | Set `POLISHD_SESSION_COOKIE`; both read it |
249
354
  | `ERR_UNKNOWN_BUILTIN_MODULE: node:sqlite` | Node < 22.5 | Upgrade Node, or configure Postgres |
250
- | No `polishd_session` cookie | Proxy file missing/misnamed, or matcher excludes the path | Check `proxy.ts` (Next 16) vs `middleware.ts` (Next 15) |
355
+ | `ExperimentalWarning: SQLite is an experimental feature` | Node's own `node:sqlite` module on 22.x | **Expected and harmless.** Not a Polishd instability. Do not "fix" it or switch stores because of it |
356
+ | No `polishd_session` cookie | Proxy file missing/misnamed, or matcher excludes the path | Check `proxy.ts` (Next 16+) vs `middleware.ts` (Next 15) |
357
+ | Dashboard content unreachable below the fold | Host `body` doesn't scroll | Fixed by default; if an ancestor sets `transform`/`filter`/`contain`, pass `shell: false` and use a route-group layout (docs/SETUP.md) |
358
+ | Server actions 500 with an opaque digest | No dashboard page registered a policy | Expected if `app/polishd/page.tsx` was deleted — actions fail closed |
251
359
  | Database drivers in the client bundle | `@polishd/next/server` imported client-side | Import from `@polishd/next/client` |
252
360
 
253
361
  ## Entry points
@@ -258,5 +366,12 @@ a notice on the dashboard, and no impact on the host app.
258
366
  | `@polishd/next/client` | `initPolishd`, `PolishdMonitor` | browser |
259
367
  | `@polishd/next/server` | store, ingest, queries, `withPolishdSession` | Node only |
260
368
  | `@polishd/next/route` | `POST`, `createPolishdRoute` | Node only |
261
- | `@polishd/next/proxy` | `proxy`, `config`, `withPolishdSession`, `polishdMatcher` | Edge-safe |
262
- | `@polishd/next/dashboard` | `createPolishdPage`, `PolishdDashboard` | Node only |
369
+ | `@polishd/next/proxy` | `proxy`, `config`, `withPolishdSession`, `polishdMatcher`, `composePolishd` | Edge-safe |
370
+ | `@polishd/next/dashboard` | `createPolishdPage`, `polishdTokenAuth`, `PolishdDashboard` | Node only |
371
+
372
+ ## CLI
373
+
374
+ | Command | Purpose |
375
+ |---|---|
376
+ | `npx @polishd/next init` | Scaffold. `--dry-run`, `--force`, `--js`, `--config` |
377
+ | `npx @polishd/next doctor` | Check the install. `--url <origin>` adds live checks |
package/CHANGELOG.md ADDED
@@ -0,0 +1,163 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ### Design review — a new dashboard tab
6
+
7
+ The dashboard now has a sidebar with two tabs: **Analytics** (everything it
8
+ showed before) and **Design**, a new aesthetic layer. The Design tab renders
9
+ your site's brand guideline *reverse-engineered from the rendered pages*:
10
+
11
+ - **Typography** — every font family / size / weight combination actually in
12
+ use, shown as specimens with real text sampled from the site.
13
+ - **Color palette** — the colors your pages paint, laid out like a brand
14
+ palette and weighted by how much of the UI they cover, split by role
15
+ (text / background / border).
16
+ - **Contrast** — every text-on-background pairing measured against WCAG AA,
17
+ failures shown with live samples.
18
+ - **Corner radii and spacing** — the rounding and padding scales in use.
19
+
20
+ Each section flags what breaks the system deterministically — no model
21
+ involved: font-size sprawl (16 sizes is not a type scale), extra families,
22
+ near-duplicate colors (`#fefefe` next to `#ffffff`), contrast failures,
23
+ one-off values that appear on a single page, and padding that drifts off a
24
+ 4px grid.
25
+
26
+ On top of the measured metrics, an **Aesthetic review** card asks the same
27
+ model you configured for the analytics summary how coherent the design reads
28
+ — strengths, and evidence-cited issues with concrete fixes. Like the summary,
29
+ it is fingerprint-cached: reopening the tab is free, and regenerating with
30
+ unchanged metrics never spends tokens. Refresh buttons for both the metrics
31
+ and the analysis sit on the tab.
32
+
33
+ Nothing new to wire up: pages measure their own rendered design once per
34
+ visitor session (a compact few-KB tally, sampled after the page settles) and
35
+ the data flows through the existing ingest route and store. The dashboard
36
+ itself is never scanned. Existing installs only need the new package version.
37
+
38
+ ## 0.2.0
39
+
40
+ Everything in this release came out of the first real third-party integration of
41
+ the package, and most of it is the same underlying problem: Polishd assumed
42
+ things about the host app that it never checked and could not see.
43
+
44
+ ### Upgrading from 0.1.x
45
+
46
+ **Styling keeps working with no changes.** Your existing `@source
47
+ ".../@polishd/next/dist"` still compiles the dashboard's utilities exactly as
48
+ it did, so upgrading will not leave you with an unstyled page. Verified by
49
+ rendering the dashboard both ways: 491 elements, no visible differences.
50
+
51
+ There is **one required change**, and it is about access, not styling.
52
+
53
+ **Decide how the dashboard is protected in production.** It now refuses to serve
54
+ unless you have chosen one:
55
+
56
+ ```bash
57
+ POLISHD_DASHBOARD_TOKEN=$(openssl rand -hex 32) # built-in gate, no code
58
+ ```
59
+
60
+ or pass your own `authenticate` to `createPolishdPage()`, or opt out explicitly
61
+ with `POLISHD_DASHBOARD_PUBLIC=true`. Development is unaffected.
62
+
63
+ Then run `npx @polishd/next doctor`, which checks that and much else.
64
+
65
+ #### Optional: move to the shipped stylesheet
66
+
67
+ Recommended, but at your own pace. **Order matters** — add the import first, and
68
+ only then remove the old wiring, so the dashboard is never unstyled in between:
69
+
70
+ 1. Add to `app/polishd/page.tsx`:
71
+ ```tsx
72
+ import "@polishd/next/dashboard.css";
73
+ ```
74
+ 2. Confirm the dashboard still looks right.
75
+ 3. *Then* delete the `@source ".../@polishd/next/dist"` line from your Tailwind
76
+ entry stylesheet (or the `content` glob in `tailwind.config.*`).
77
+
78
+ Doing this drops Tailwind as a requirement for your app and stops your build
79
+ compiling utilities only the dashboard uses. `doctor` reports which of the two
80
+ setups it finds, and never calls a working 0.1.x setup broken.
81
+
82
+ #### If you are on Next 15, check this
83
+
84
+ 0.1.0's CLI wrote `proxy.ts` regardless of the installed Next major, and **Next
85
+ 15 does not load that file**. If you are on Next 15 your proxy has never run:
86
+ no session cookie, every beacon returning 200, and nothing captured, with no
87
+ error anywhere to show for it. Rename it to `middleware.ts` and rename the
88
+ exported function to `middleware`. `doctor` reports this explicitly.
89
+
90
+ ### Breaking
91
+
92
+ - **The dashboard fails shut in production.** With no `POLISHD_DASHBOARD_TOKEN`,
93
+ no `authenticate` callback and no `POLISHD_DASHBOARD_PUBLIC=true`, production
94
+ renders a locked screen and every AI server action denies. Previously the
95
+ default was open, and the warning about it was suppressed in production —
96
+ exactly where it was worth acting on. The dashboard's settings can change the
97
+ configured model and API base URL, so an open one is arbitrary spend on the
98
+ owner's key, not merely an analytics leak.
99
+ - **The dashboard renders in its own shell** (`position: fixed`, own background
100
+ and scroll container) rather than inheriting the host page. Pass
101
+ `shell: false` to `createPolishdPage()` for the old behaviour.
102
+
103
+ ### Added
104
+
105
+ - **`npx @polishd/next doctor`** — checks the wiring that fails silently: the
106
+ proxy named and exported for the installed Next major, `config.matcher` as an
107
+ inline literal excluding `/api`, `runtime`/`dynamic` declared rather than
108
+ re-exported, the stylesheet import, access posture, Node version.
109
+ `--url <origin>` adds live checks that the cookie is minted and events store.
110
+ - **Built-in token auth.** Setting `POLISHD_DASHBOARD_TOKEN` is the whole
111
+ integration for an app with no auth of its own — no import, no page edit. The
112
+ token is exchanged for an httpOnly, path-scoped cookie by a server action, so
113
+ it never appears in a URL, in history, in logs or in a `Referer` header. The
114
+ cookie holds an HMAC rather than the token, comparison is constant-time,
115
+ expiry is enforced server-side, and the unlock form is rate limited.
116
+ `polishdTokenAuth()` is exported for hosts that want to configure it.
117
+ - **`dist/dashboard.css`** — the dashboard ships its own stylesheet, compiled at
118
+ publish time. Tailwind is no longer required in the host app.
119
+ - **`composePolishd(handler, { matcher })`** — for apps that already have a
120
+ proxy. Declaring the handler's original scope re-applies it as an internal
121
+ guard, so widening `config.matcher` to the union doesn't silently start
122
+ running your handler on every request in the app.
123
+ - **`authenticate` receives the request context** (`cookies`, `headers`), so
124
+ callbacks are ordinary functions of their inputs and testable without a live
125
+ request. Zero-argument callbacks keep working unchanged.
126
+ - **`POLISHD_SESSION_COOKIE`** — read by both the proxy and the ingest route, so
127
+ renaming the cookie cannot go half-applied.
128
+
129
+ ### Fixed
130
+
131
+ - **`--force` destroyed hand-written proxies.** Both the exists-guard and the
132
+ merge hint were skipped under the flag, so a user who passed it to regenerate
133
+ an unrelated file lost their real proxy wholesale. Generated files now carry a
134
+ sentinel; files the CLI did not author are never overwritten, with or without
135
+ `--force`, and its own files are backed up to `.bak` before regenerating.
136
+ - **The CLI ignored the installed Next major**, always writing `proxy.ts`. On
137
+ Next 15 — a supported version — that file never runs: no session cookie, every
138
+ beacon still 200, nothing stored, no error anywhere. It now resolves the
139
+ installed version and picks the filename and exported function to match.
140
+ - **The merge hint keyed off the wrong filename**, so the Next 16 collision —
141
+ the one that actually happens — got no guidance and only `--force` as a
142
+ remedy, which was the data-loss bug above.
143
+ - **A missing session cookie is no longer silent.** Ingest records the rejection
144
+ in `polishd_meta`, the dashboard shows a banner naming the expected proxy
145
+ filename per Next major, and the client warns once in the dev console.
146
+ - **`--dry-run` under-reported**, omitting the `.gitignore` change.
147
+ - **`polishd.config.ts` was documented as being loaded automatically.** Nothing
148
+ imports it; it cannot be auto-loaded, because the three consumers run in three
149
+ runtimes and the Edge one has no filesystem. The docs now say what actually
150
+ has to happen.
151
+
152
+ ### Also
153
+
154
+ - Node `>= 22.5.0` is checked by the CLI rather than left to a confusing
155
+ "cannot find module `node:sqlite`" at first request.
156
+ - `bin/` is included in the published files. npm auto-includes the bin entry but
157
+ not the modules it imports, so the CLI would otherwise have shipped broken.
158
+ - Docs carry a paste-ready prompt pointing a coding agent at `AGENTS.md`, in the
159
+ README, `docs/SETUP.md` and `polishd --help`.
160
+
161
+ ## 0.1.0
162
+
163
+ Initial release.