@polishd/next 0.1.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 +262 -0
- package/DATABASE.md +147 -0
- package/LICENSE +21 -0
- package/README.md +310 -0
- package/bin/init.mjs +224 -0
- package/dist/ai/actions.d.ts +23 -0
- package/dist/ai/actions.d.ts.map +1 -0
- package/dist/ai/actions.js +58 -0
- package/dist/ai/actions.js.map +1 -0
- package/dist/ai/digest.d.ts +9 -0
- package/dist/ai/digest.d.ts.map +1 -0
- package/dist/ai/digest.js +108 -0
- package/dist/ai/digest.js.map +1 -0
- package/dist/ai/github.d.ts +51 -0
- package/dist/ai/github.d.ts.map +1 -0
- package/dist/ai/github.js +175 -0
- package/dist/ai/github.js.map +1 -0
- package/dist/ai/guard.d.ts +43 -0
- package/dist/ai/guard.d.ts.map +1 -0
- package/dist/ai/guard.js +57 -0
- package/dist/ai/guard.js.map +1 -0
- package/dist/ai/issues.d.ts +21 -0
- package/dist/ai/issues.d.ts.map +1 -0
- package/dist/ai/issues.js +291 -0
- package/dist/ai/issues.js.map +1 -0
- package/dist/ai/profile.d.ts +26 -0
- package/dist/ai/profile.d.ts.map +1 -0
- package/dist/ai/profile.js +160 -0
- package/dist/ai/profile.js.map +1 -0
- package/dist/ai/providers.d.ts +40 -0
- package/dist/ai/providers.d.ts.map +1 -0
- package/dist/ai/providers.js +142 -0
- package/dist/ai/providers.js.map +1 -0
- package/dist/ai/scan.d.ts +38 -0
- package/dist/ai/scan.d.ts.map +1 -0
- package/dist/ai/scan.js +274 -0
- package/dist/ai/scan.js.map +1 -0
- package/dist/ai/settings.d.ts +31 -0
- package/dist/ai/settings.d.ts.map +1 -0
- package/dist/ai/settings.js +182 -0
- package/dist/ai/settings.js.map +1 -0
- package/dist/ai/summary.d.ts +26 -0
- package/dist/ai/summary.d.ts.map +1 -0
- package/dist/ai/summary.js +260 -0
- package/dist/ai/summary.js.map +1 -0
- package/dist/ai/types.d.ts +188 -0
- package/dist/ai/types.d.ts.map +1 -0
- package/dist/ai/types.js +13 -0
- package/dist/ai/types.js.map +1 -0
- package/dist/client/init.d.ts +14 -0
- package/dist/client/init.d.ts.map +1 -0
- package/dist/client/init.js +274 -0
- package/dist/client/init.js.map +1 -0
- package/dist/client/monitor.d.ts +23 -0
- package/dist/client/monitor.d.ts.map +1 -0
- package/dist/client/monitor.js +176 -0
- package/dist/client/monitor.js.map +1 -0
- package/dist/client.d.ts +11 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +10 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +26 -0
- package/dist/config.js.map +1 -0
- package/dist/dashboard/elements.d.ts +6 -0
- package/dist/dashboard/elements.d.ts.map +1 -0
- package/dist/dashboard/elements.js +35 -0
- package/dist/dashboard/elements.js.map +1 -0
- package/dist/dashboard/features.d.ts +6 -0
- package/dist/dashboard/features.d.ts.map +1 -0
- package/dist/dashboard/features.js +28 -0
- package/dist/dashboard/features.js.map +1 -0
- package/dist/dashboard/index.d.ts +54 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/index.js +140 -0
- package/dist/dashboard/index.js.map +1 -0
- package/dist/dashboard/journeys.d.ts +5 -0
- package/dist/dashboard/journeys.d.ts.map +1 -0
- package/dist/dashboard/journeys.js +158 -0
- package/dist/dashboard/journeys.js.map +1 -0
- package/dist/dashboard/pages.d.ts +5 -0
- package/dist/dashboard/pages.d.ts.map +1 -0
- package/dist/dashboard/pages.js +104 -0
- package/dist/dashboard/pages.js.map +1 -0
- package/dist/dashboard/summary.d.ts +13 -0
- package/dist/dashboard/summary.d.ts.map +1 -0
- package/dist/dashboard/summary.js +333 -0
- package/dist/dashboard/summary.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/proxy.d.ts +40 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +29 -0
- package/dist/proxy.js.map +1 -0
- package/dist/route.d.ts +24 -0
- package/dist/route.d.ts.map +1 -0
- package/dist/route.js +37 -0
- package/dist/route.js.map +1 -0
- package/dist/server/ingest.d.ts +14 -0
- package/dist/server/ingest.d.ts.map +1 -0
- package/dist/server/ingest.js +73 -0
- package/dist/server/ingest.js.map +1 -0
- package/dist/server/queries.d.ts +245 -0
- package/dist/server/queries.d.ts.map +1 -0
- package/dist/server/queries.js +547 -0
- package/dist/server/queries.js.map +1 -0
- package/dist/server/store.d.ts +29 -0
- package/dist/server/store.d.ts.map +1 -0
- package/dist/server/store.js +359 -0
- package/dist/server/store.js.map +1 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +15 -0
- package/dist/server.js.map +1 -0
- package/dist/session.d.ts +16 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +29 -0
- package/dist/session.js.map +1 -0
- package/dist/shared/types.d.ts +56 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +27 -0
- package/dist/shared/types.js.map +1 -0
- package/docs/SETUP.md +315 -0
- package/package.json +92 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* PolishdMonitor — explicit component-level tracking for Polishd.
|
|
5
|
+
*
|
|
6
|
+
* Drop this around any element you want to monitor. Two tiers of tracking:
|
|
7
|
+
*
|
|
8
|
+
* Always on (cheap, suits interactive controls like buttons):
|
|
9
|
+
* • hover — pointer dwell time (≥200ms), value = ms
|
|
10
|
+
* • child clicks/rage/dead — attributed automatically via data-component and
|
|
11
|
+
* the existing global Polishd click capture, no extra wiring needed.
|
|
12
|
+
*
|
|
13
|
+
* Opt-in with `content` (for content regions like an article body):
|
|
14
|
+
* • mount — fired once when the region renders, so you can count how often
|
|
15
|
+
* it was opened/shown.
|
|
16
|
+
* • component_view — time in the viewport per visit, value = ms, meta includes:
|
|
17
|
+
* width, height (px), scrollDepth (% of component scrolled through),
|
|
18
|
+
* views (how many times it entered the viewport)
|
|
19
|
+
*
|
|
20
|
+
* Leave `content` off for clickable elements so we don't measure meaningless
|
|
21
|
+
* height/scroll/viewport data on them.
|
|
22
|
+
*
|
|
23
|
+
* Usage:
|
|
24
|
+
* <PolishdMonitor name="listen-button"> // hover + click only
|
|
25
|
+
* <ListenButton src={src} />
|
|
26
|
+
* </PolishdMonitor>
|
|
27
|
+
*
|
|
28
|
+
* <PolishdMonitor name={slug} content> // + mount, view, scroll, height
|
|
29
|
+
* <Article />
|
|
30
|
+
* </PolishdMonitor>
|
|
31
|
+
*/
|
|
32
|
+
import { useEffect, useRef } from "react";
|
|
33
|
+
/** Find the nearest ancestor that scrolls on the y-axis. */
|
|
34
|
+
function findScrollParent(el) {
|
|
35
|
+
let node = el.parentElement;
|
|
36
|
+
while (node) {
|
|
37
|
+
const { overflowY } = window.getComputedStyle(node);
|
|
38
|
+
if (/auto|scroll/.test(overflowY) && node.scrollHeight > node.clientHeight)
|
|
39
|
+
return node;
|
|
40
|
+
node = node.parentElement;
|
|
41
|
+
}
|
|
42
|
+
return window;
|
|
43
|
+
}
|
|
44
|
+
export function PolishdMonitor({ name, children, className, content = false }) {
|
|
45
|
+
const ref = useRef(null);
|
|
46
|
+
const hoverStart = useRef(null);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const el = ref.current;
|
|
49
|
+
if (!el)
|
|
50
|
+
return;
|
|
51
|
+
// Auto-detect what's trackable and annotate for devtools.
|
|
52
|
+
const hasButtons = el.querySelectorAll("button").length > 0;
|
|
53
|
+
const hasLinks = el.querySelectorAll("a").length > 0;
|
|
54
|
+
const kinds = ["hover"];
|
|
55
|
+
if (content)
|
|
56
|
+
kinds.push("mount", "view");
|
|
57
|
+
if (hasButtons)
|
|
58
|
+
kinds.push("click");
|
|
59
|
+
if (hasLinks)
|
|
60
|
+
kinds.push("link");
|
|
61
|
+
el.dataset.polishdTracks = kinds.join(",");
|
|
62
|
+
// ── Hover tracking (always on) ──────────────────────────────────────────
|
|
63
|
+
const onEnter = () => { hoverStart.current = Date.now(); };
|
|
64
|
+
const onLeave = () => {
|
|
65
|
+
if (hoverStart.current === null)
|
|
66
|
+
return;
|
|
67
|
+
const ms = Date.now() - hoverStart.current;
|
|
68
|
+
hoverStart.current = null;
|
|
69
|
+
if (ms < 200)
|
|
70
|
+
return;
|
|
71
|
+
window.__polishdTrack?.({ type: "hover", component: name, value: ms });
|
|
72
|
+
};
|
|
73
|
+
el.addEventListener("pointerenter", onEnter);
|
|
74
|
+
el.addEventListener("pointerleave", onLeave);
|
|
75
|
+
// Interactive controls (the default) stop here — no viewport/scroll/mount.
|
|
76
|
+
if (!content) {
|
|
77
|
+
return () => {
|
|
78
|
+
el.removeEventListener("pointerenter", onEnter);
|
|
79
|
+
el.removeEventListener("pointerleave", onLeave);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// ── Content-region tracking (content=true only) ─────────────────────────
|
|
83
|
+
// Record that the region rendered, so we can count how often it was shown.
|
|
84
|
+
window.__polishdTrack?.({ type: "mount", component: name });
|
|
85
|
+
// ── Viewport / focus tracking ───────────────────────────────────────────
|
|
86
|
+
// visibleSince: when this viewport visit started (null = not visible)
|
|
87
|
+
// totalMs: accumulated visible time across all viewport visits
|
|
88
|
+
// maxScrollDepth: highest scroll-through % seen (0–100)
|
|
89
|
+
// maxHeightPx/maxWidthPx: largest rendered box seen while laid out
|
|
90
|
+
// viewCount: how many times the component entered the viewport
|
|
91
|
+
let visibleSince = null;
|
|
92
|
+
let totalMs = 0;
|
|
93
|
+
let maxScrollDepth = 0;
|
|
94
|
+
let maxHeightPx = 0;
|
|
95
|
+
let maxWidthPx = 0;
|
|
96
|
+
let viewCount = 0;
|
|
97
|
+
// Track scroll-through % and the element's rendered size *while it's laid
|
|
98
|
+
// out*. getBoundingClientRect returns the full box (not clipped to the
|
|
99
|
+
// viewport), so it captures the element's true height even when only part
|
|
100
|
+
// is on screen — and reading it here, rather than el.offsetHeight at emit
|
|
101
|
+
// time, avoids the 0px we'd otherwise get when emitView fires during React
|
|
102
|
+
// teardown (unmount / name change).
|
|
103
|
+
// Scroll-depth formula: (viewportHeight - componentTop) / componentHeight,
|
|
104
|
+
// clamped 0–100. Reaches 100% when the component's bottom is at the
|
|
105
|
+
// viewport bottom.
|
|
106
|
+
const refreshScrollDepth = () => {
|
|
107
|
+
const rect = el.getBoundingClientRect();
|
|
108
|
+
if (rect.height === 0)
|
|
109
|
+
return;
|
|
110
|
+
if (rect.height > maxHeightPx)
|
|
111
|
+
maxHeightPx = rect.height;
|
|
112
|
+
if (rect.width > maxWidthPx)
|
|
113
|
+
maxWidthPx = rect.width;
|
|
114
|
+
const pct = Math.max(0, Math.min(100, Math.round(((window.innerHeight - rect.top) / rect.height) * 100)));
|
|
115
|
+
if (pct > maxScrollDepth)
|
|
116
|
+
maxScrollDepth = pct;
|
|
117
|
+
};
|
|
118
|
+
// Flush accumulated viewport time as a component_view event.
|
|
119
|
+
// Called when the component leaves the viewport or the effect cleans up.
|
|
120
|
+
const emitView = () => {
|
|
121
|
+
if (visibleSince !== null) {
|
|
122
|
+
totalMs += Date.now() - visibleSince;
|
|
123
|
+
visibleSince = null;
|
|
124
|
+
}
|
|
125
|
+
if (totalMs < 500)
|
|
126
|
+
return; // ignore flashes shorter than half a second
|
|
127
|
+
window.__polishdTrack?.({
|
|
128
|
+
type: "component_view",
|
|
129
|
+
component: name,
|
|
130
|
+
value: totalMs,
|
|
131
|
+
meta: {
|
|
132
|
+
width: Math.round(maxWidthPx),
|
|
133
|
+
height: Math.round(maxHeightPx),
|
|
134
|
+
scrollDepth: maxScrollDepth,
|
|
135
|
+
views: viewCount,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
totalMs = 0;
|
|
139
|
+
maxScrollDepth = 0;
|
|
140
|
+
maxHeightPx = 0;
|
|
141
|
+
maxWidthPx = 0;
|
|
142
|
+
viewCount = 0;
|
|
143
|
+
};
|
|
144
|
+
const observer = new IntersectionObserver((entries) => {
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
if (entry.isIntersecting) {
|
|
147
|
+
if (visibleSince === null) {
|
|
148
|
+
visibleSince = Date.now();
|
|
149
|
+
viewCount++;
|
|
150
|
+
}
|
|
151
|
+
refreshScrollDepth();
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
if (visibleSince !== null) {
|
|
155
|
+
totalMs += Date.now() - visibleSince;
|
|
156
|
+
visibleSince = null;
|
|
157
|
+
}
|
|
158
|
+
if (totalMs >= 500)
|
|
159
|
+
emitView();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}, { threshold: [0, 0.1, 0.5, 1.0] });
|
|
163
|
+
observer.observe(el);
|
|
164
|
+
const scrollParent = findScrollParent(el);
|
|
165
|
+
scrollParent.addEventListener("scroll", refreshScrollDepth, { passive: true });
|
|
166
|
+
return () => {
|
|
167
|
+
emitView(); // flush remaining time on unmount / name change
|
|
168
|
+
observer.disconnect();
|
|
169
|
+
scrollParent.removeEventListener("scroll", refreshScrollDepth);
|
|
170
|
+
el.removeEventListener("pointerenter", onEnter);
|
|
171
|
+
el.removeEventListener("pointerleave", onLeave);
|
|
172
|
+
};
|
|
173
|
+
}, [name, content]);
|
|
174
|
+
return (_jsx("span", { ref: ref, "data-component": name, className: className, children: children }));
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=monitor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"monitor.js","sourceRoot":"","sources":["../../src/client/monitor.tsx"],"names":[],"mappings":"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"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @polishd/next/client — browser capture surface.
|
|
3
|
+
*
|
|
4
|
+
* `initPolishd` is framework-free vanilla DOM (call it from
|
|
5
|
+
* `instrumentation-client.ts`). `PolishdMonitor` is a React component for
|
|
6
|
+
* explicit, component-level tracking.
|
|
7
|
+
*/
|
|
8
|
+
export { initPolishd } from "./client/init";
|
|
9
|
+
export { PolishdMonitor } from "./client/monitor";
|
|
10
|
+
export type { PolishdConfig } from "./config";
|
|
11
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @polishd/next/client — browser capture surface.
|
|
3
|
+
*
|
|
4
|
+
* `initPolishd` is framework-free vanilla DOM (call it from
|
|
5
|
+
* `instrumentation-client.ts`). `PolishdMonitor` is a React component for
|
|
6
|
+
* explicit, component-level tracking.
|
|
7
|
+
*/
|
|
8
|
+
export { initPolishd } from "./client/init";
|
|
9
|
+
export { PolishdMonitor } from "./client/monitor";
|
|
10
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polishd — shared configuration.
|
|
3
|
+
*
|
|
4
|
+
* This object holds ONLY non-secret, client-safe settings: it is imported by
|
|
5
|
+
* both the browser capture layer and the server. Secrets (database URLs,
|
|
6
|
+
* Anthropic keys) never live here — they are read from `process.env` on the
|
|
7
|
+
* server only. See `server/store.ts`.
|
|
8
|
+
*
|
|
9
|
+
* The host app can override any of these by editing the root `polishd.config.ts`
|
|
10
|
+
* re-export, which keeps the public surface in one obvious place.
|
|
11
|
+
*/
|
|
12
|
+
export interface PolishdConfig {
|
|
13
|
+
/** Whether capture is active at all. Disable to fully no-op the client. */
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
/** Route the client flushes batches to. Must match the api route handler. */
|
|
16
|
+
apiRoute: string;
|
|
17
|
+
/** Flush cadence in ms. The client also flushes on pagehide. */
|
|
18
|
+
flushIntervalMs: number;
|
|
19
|
+
/** Max events held before forcing an early flush (caps memory + payload). */
|
|
20
|
+
maxBatchSize: number;
|
|
21
|
+
/** Fraction of sessions to capture, 0–1. 1 = everyone. */
|
|
22
|
+
sampleRate: number;
|
|
23
|
+
/** Rage-click detection: N clicks on one element within `windowMs`. */
|
|
24
|
+
rageClick: {
|
|
25
|
+
count: number;
|
|
26
|
+
windowMs: number;
|
|
27
|
+
};
|
|
28
|
+
/** Local SQLite file used in dev. Ignored when POLISHD_DATABASE_URL is set. */
|
|
29
|
+
databasePath: string;
|
|
30
|
+
/** Name of the anonymous session cookie set by the middleware. */
|
|
31
|
+
sessionCookie: string;
|
|
32
|
+
}
|
|
33
|
+
export declare const defaultPolishdConfig: PolishdConfig;
|
|
34
|
+
/** Type-checked helper for the host app's root `polishd.config.ts`. */
|
|
35
|
+
export declare function definePolishdConfig(overrides: Partial<PolishdConfig>): PolishdConfig;
|
|
36
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,aAAa;IAC5B,2EAA2E;IAC3E,OAAO,EAAE,OAAO,CAAC;IACjB,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,eAAe,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,YAAY,EAAE,MAAM,CAAC;IACrB,0DAA0D;IAC1D,UAAU,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,SAAS,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,+EAA+E;IAC/E,YAAY,EAAE,MAAM,CAAC;IACrB,kEAAkE;IAClE,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,eAAO,MAAM,oBAAoB,EAAE,aASlC,CAAC;AAEF,uEAAuE;AACvE,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,aAAa,CAEpF"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polishd — shared configuration.
|
|
3
|
+
*
|
|
4
|
+
* This object holds ONLY non-secret, client-safe settings: it is imported by
|
|
5
|
+
* both the browser capture layer and the server. Secrets (database URLs,
|
|
6
|
+
* Anthropic keys) never live here — they are read from `process.env` on the
|
|
7
|
+
* server only. See `server/store.ts`.
|
|
8
|
+
*
|
|
9
|
+
* The host app can override any of these by editing the root `polishd.config.ts`
|
|
10
|
+
* re-export, which keeps the public surface in one obvious place.
|
|
11
|
+
*/
|
|
12
|
+
export const defaultPolishdConfig = {
|
|
13
|
+
enabled: true,
|
|
14
|
+
apiRoute: "/api/polishd",
|
|
15
|
+
flushIntervalMs: 10_000,
|
|
16
|
+
maxBatchSize: 50,
|
|
17
|
+
sampleRate: 1,
|
|
18
|
+
rageClick: { count: 3, windowMs: 500 },
|
|
19
|
+
databasePath: ".polishd/analytics.db",
|
|
20
|
+
sessionCookie: "polishd_session",
|
|
21
|
+
};
|
|
22
|
+
/** Type-checked helper for the host app's root `polishd.config.ts`. */
|
|
23
|
+
export function definePolishdConfig(overrides) {
|
|
24
|
+
return { ...defaultPolishdConfig, ...overrides };
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAqBH,MAAM,CAAC,MAAM,oBAAoB,GAAkB;IACjD,OAAO,EAAE,IAAI;IACb,QAAQ,EAAE,cAAc;IACxB,eAAe,EAAE,MAAM;IACvB,YAAY,EAAE,EAAE;IAChB,UAAU,EAAE,CAAC;IACb,SAAS,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE;IACtC,YAAY,EAAE,uBAAuB;IACrC,aAAa,EAAE,iBAAiB;CACjC,CAAC;AAEF,uEAAuE;AACvE,MAAM,UAAU,mBAAmB,CAAC,SAAiC;IACnE,OAAO,EAAE,GAAG,oBAAoB,EAAE,GAAG,SAAS,EAAE,CAAC;AACnD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"elements.d.ts","sourceRoot":"","sources":["../../src/dashboard/elements.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAkCrD,MAAM,CAAC,OAAO,UAAU,aAAa,CAAC,EACpC,QAAQ,EACR,MAAM,GACP,EAAE;IACD,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,MAAM,EAAE,KAAK,CAAC,SAAS,CAAC;CACzB,+BA+BA"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* Polishd — "Interactions by element" body (client).
|
|
5
|
+
*
|
|
6
|
+
* Renders the element rows for the element-issues table, collapsed to the
|
|
7
|
+
* three most recently interacted-with elements by default with a "Show more"
|
|
8
|
+
* toggle to reveal the rest. Lives in its own client component so the toggle is
|
|
9
|
+
* interactive while the surrounding dashboard stays a server component.
|
|
10
|
+
*
|
|
11
|
+
* Returns a <tbody>/<tfoot> fragment so it slots straight into the table whose
|
|
12
|
+
* <thead> is rendered by the server page.
|
|
13
|
+
*/
|
|
14
|
+
import { useState } from "react";
|
|
15
|
+
const border = "border-[#2e2e2e]";
|
|
16
|
+
const divider = `border-t ${border}`;
|
|
17
|
+
/** Number of elements shown before "Show more" is needed. */
|
|
18
|
+
const COLLAPSED_COUNT = 3;
|
|
19
|
+
function scoreTone(score) {
|
|
20
|
+
if (score >= 10)
|
|
21
|
+
return "text-red-500";
|
|
22
|
+
if (score >= 4)
|
|
23
|
+
return "text-[#f5a623]";
|
|
24
|
+
return "text-[#0cce6b]";
|
|
25
|
+
}
|
|
26
|
+
function ElementRow({ el }) {
|
|
27
|
+
return (_jsxs("tr", { className: `${divider} align-top`, children: [_jsxs("td", { className: "py-2.5 pl-5 pr-6", children: [_jsx("span", { className: "font-mono text-[13px] font-medium text-white", children: el.label }), el.sampleText && (_jsxs("div", { className: "mt-0.5 font-mono text-[11px] text-[#555]", children: ["\"", el.sampleText, "\""] }))] }), _jsx("td", { className: `py-2.5 px-4 text-right text-[13px] font-semibold tabular-nums ${scoreTone(el.score)}`, children: el.score }), _jsx("td", { className: "py-2.5 px-4 text-right text-[13px] tabular-nums text-[#888]", children: el.clicks }), _jsx("td", { className: "py-2.5 px-4 text-right text-[13px] tabular-nums text-[#888]", children: el.rageClicks }), _jsx("td", { className: "py-2.5 px-4 text-right text-[13px] tabular-nums text-[#888]", children: el.deadClicks }), _jsx("td", { className: "py-2.5 pl-4 pr-5 text-right text-[13px] tabular-nums text-[#888]", children: el.pages })] }));
|
|
28
|
+
}
|
|
29
|
+
export default function ElementsTable({ elements, header, }) {
|
|
30
|
+
const [expanded, setExpanded] = useState(false);
|
|
31
|
+
const hidden = elements.length - COLLAPSED_COUNT;
|
|
32
|
+
const shown = expanded ? elements : elements.slice(0, COLLAPSED_COUNT);
|
|
33
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full min-w-[560px]", children: [_jsx("thead", { children: header }), _jsx("tbody", { children: shown.map((el) => (_jsx(ElementRow, { el: el }, el.label))) })] }) }), hidden > 0 && (_jsx("div", { className: `${divider} px-5 py-2.5 text-center`, children: _jsx("button", { type: "button", onClick: () => setExpanded((v) => !v), className: "text-[12px] font-medium text-[#888] transition-colors hover:text-white", children: expanded ? "Show less" : `Show ${hidden} more` }) }))] }));
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=elements.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"elements.js","sourceRoot":"","sources":["../../src/dashboard/elements.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb;;;;;;;;;;GAUG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAIjC,MAAM,MAAM,GAAG,kBAAkB,CAAC;AAClC,MAAM,OAAO,GAAG,YAAY,MAAM,EAAE,CAAC;AAErC,6DAA6D;AAC7D,MAAM,eAAe,GAAG,CAAC,CAAC;AAE1B,SAAS,SAAS,CAAC,KAAa;IAC9B,IAAI,KAAK,IAAI,EAAE;QAAE,OAAO,cAAc,CAAC;IACvC,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,gBAAgB,CAAC;IACxC,OAAO,gBAAgB,CAAC;AAC1B,CAAC;AAED,SAAS,UAAU,CAAC,EAAE,EAAE,EAAuB;IAC7C,OAAO,CACL,cAAI,SAAS,EAAE,GAAG,OAAO,YAAY,aACnC,cAAI,SAAS,EAAC,kBAAkB,aAC9B,eAAM,SAAS,EAAC,8CAA8C,YAAE,EAAE,CAAC,KAAK,GAAQ,EAC/E,EAAE,CAAC,UAAU,IAAI,CAChB,eAAK,SAAS,EAAC,0CAA0C,mBAAG,EAAE,CAAC,UAAU,UAAQ,CAClF,IACE,EACL,aAAI,SAAS,EAAE,iEAAiE,SAAS,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,YAClG,EAAE,CAAC,KAAK,GACN,EACL,aAAI,SAAS,EAAC,6DAA6D,YAAE,EAAE,CAAC,MAAM,GAAM,EAC5F,aAAI,SAAS,EAAC,6DAA6D,YAAE,EAAE,CAAC,UAAU,GAAM,EAChG,aAAI,SAAS,EAAC,6DAA6D,YAAE,EAAE,CAAC,UAAU,GAAM,EAChG,aAAI,SAAS,EAAC,kEAAkE,YAAE,EAAE,CAAC,KAAK,GAAM,IAC7F,CACN,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,OAAO,UAAU,aAAa,CAAC,EACpC,QAAQ,EACR,MAAM,GAIP;IACC,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,GAAG,eAAe,CAAC;IACjD,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,eAAe,CAAC,CAAC;IAEvE,OAAO,CACL,8BAEE,cAAK,SAAS,EAAC,iBAAiB,YAC9B,iBAAO,SAAS,EAAC,sBAAsB,aACrC,0BAAQ,MAAM,GAAS,EACvB,0BACG,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CACjB,KAAC,UAAU,IAAgB,EAAE,EAAE,EAAE,IAAhB,EAAE,CAAC,KAAK,CAAY,CACtC,CAAC,GACI,IACF,GACJ,EACL,MAAM,GAAG,CAAC,IAAI,CACb,cAAK,SAAS,EAAE,GAAG,OAAO,0BAA0B,YAClD,iBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EACrC,SAAS,EAAC,wEAAwE,YAEjF,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,MAAM,OAAO,GACxC,GACL,CACP,IACA,CACJ,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"features.d.ts","sourceRoot":"","sources":["../../src/dashboard/features.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAsCxD,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,EACvC,QAAQ,EACR,MAAM,GACP,EAAE;IACD,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,MAAM,EAAE,KAAK,CAAC,SAAS,CAAC;CACzB,+BA+BA"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* Polishd — "Most-used features" body (client).
|
|
5
|
+
*
|
|
6
|
+
* Renders the most-used (highest click-volume) interactive elements, collapsed
|
|
7
|
+
* to the top three by default with a "Show more" toggle to reveal the rest.
|
|
8
|
+
* Ordering is left as the server provides it (by click volume) — that ranking
|
|
9
|
+
* is the whole point of "most-used".
|
|
10
|
+
*
|
|
11
|
+
* Returns a <tbody>/<tfoot> fragment so it slots into the table whose <thead>
|
|
12
|
+
* is rendered by the server page.
|
|
13
|
+
*/
|
|
14
|
+
import { useState } from "react";
|
|
15
|
+
const border = "border-[#2e2e2e]";
|
|
16
|
+
const divider = `border-t ${border}`;
|
|
17
|
+
/** Number of features shown before "Show more" is needed. */
|
|
18
|
+
const COLLAPSED_COUNT = 3;
|
|
19
|
+
function TopInteractionRow({ el }) {
|
|
20
|
+
return (_jsxs("tr", { className: `${divider} align-top`, children: [_jsxs("td", { className: "py-2.5 pl-5 pr-6", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-[13px] font-medium text-white", children: el.label }), _jsx("span", { className: `rounded px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wider ${el.isComponent ? "bg-blue-950 text-blue-400" : "bg-[#1a1a1a] text-[#555]"}`, children: el.isComponent ? "component" : "selector" })] }), el.sampleText && (_jsxs("div", { className: "mt-0.5 font-mono text-[11px] text-[#555]", children: ["\"", el.sampleText, "\""] })), el.isComponent && el.selector && (_jsx("div", { className: "mt-0.5 font-mono text-[11px] text-[#444]", children: el.selector }))] }), _jsx("td", { className: "py-2.5 px-4 text-right text-[13px] font-semibold tabular-nums text-white", children: el.clicks }), _jsx("td", { className: "py-2.5 px-4 text-right text-[13px] tabular-nums text-[#888]", children: el.sessions }), _jsx("td", { className: "py-2.5 pl-4 pr-5 text-right text-[13px] tabular-nums text-[#888]", children: el.pages })] }));
|
|
21
|
+
}
|
|
22
|
+
export default function TopFeaturesTable({ features, header, }) {
|
|
23
|
+
const [expanded, setExpanded] = useState(false);
|
|
24
|
+
const hidden = features.length - COLLAPSED_COUNT;
|
|
25
|
+
const shown = expanded ? features : features.slice(0, COLLAPSED_COUNT);
|
|
26
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full min-w-[420px]", children: [_jsx("thead", { children: header }), _jsx("tbody", { children: shown.map((el) => (_jsx(TopInteractionRow, { el: el }, el.label))) })] }) }), hidden > 0 && (_jsx("div", { className: `${divider} px-5 py-2.5 text-center`, children: _jsx("button", { type: "button", onClick: () => setExpanded((v) => !v), className: "text-[12px] font-medium text-[#888] transition-colors hover:text-white", children: expanded ? "Show less" : `Show ${hidden} more` }) }))] }));
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=features.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"features.js","sourceRoot":"","sources":["../../src/dashboard/features.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb;;;;;;;;;;GAUG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAIjC,MAAM,MAAM,GAAG,kBAAkB,CAAC;AAClC,MAAM,OAAO,GAAG,YAAY,MAAM,EAAE,CAAC;AAErC,6DAA6D;AAC7D,MAAM,eAAe,GAAG,CAAC,CAAC;AAE1B,SAAS,iBAAiB,CAAC,EAAE,EAAE,EAA0B;IACvD,OAAO,CACL,cAAI,SAAS,EAAE,GAAG,OAAO,YAAY,aACnC,cAAI,SAAS,EAAC,kBAAkB,aAC9B,eAAK,SAAS,EAAC,yBAAyB,aACtC,eAAM,SAAS,EAAC,oCAAoC,YAAE,EAAE,CAAC,KAAK,GAAQ,EACtE,eACE,SAAS,EAAE,2EACT,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,2BAA2B,CAAC,CAAC,CAAC,0BACjD,EAAE,YAED,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU,GACrC,IACH,EACL,EAAE,CAAC,UAAU,IAAI,CAChB,eAAK,SAAS,EAAC,0CAA0C,mBAAG,EAAE,CAAC,UAAU,UAAQ,CAClF,EACA,EAAE,CAAC,WAAW,IAAI,EAAE,CAAC,QAAQ,IAAI,CAChC,cAAK,SAAS,EAAC,0CAA0C,YAAE,EAAE,CAAC,QAAQ,GAAO,CAC9E,IACE,EACL,aAAI,SAAS,EAAC,0EAA0E,YACrF,EAAE,CAAC,MAAM,GACP,EACL,aAAI,SAAS,EAAC,6DAA6D,YAAE,EAAE,CAAC,QAAQ,GAAM,EAC9F,aAAI,SAAS,EAAC,kEAAkE,YAAE,EAAE,CAAC,KAAK,GAAM,IAC7F,CACN,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,EACvC,QAAQ,EACR,MAAM,GAIP;IACC,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,GAAG,eAAe,CAAC;IACjD,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,eAAe,CAAC,CAAC;IAEvE,OAAO,CACL,8BAEE,cAAK,SAAS,EAAC,iBAAiB,YAC9B,iBAAO,SAAS,EAAC,sBAAsB,aACrC,0BAAQ,MAAM,GAAS,EACvB,0BACG,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CACjB,KAAC,iBAAiB,IAAgB,EAAE,EAAE,EAAE,IAAhB,EAAE,CAAC,KAAK,CAAY,CAC7C,CAAC,GACI,IACF,GACJ,EACL,MAAM,GAAG,CAAC,IAAI,CACb,cAAK,SAAS,EAAE,GAAG,OAAO,0BAA0B,YAClD,iBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EACrC,SAAS,EAAC,wEAAwE,YAEjF,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,MAAM,OAAO,GACxC,GACL,CACP,IACA,CACJ,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @polishd/next/dashboard — the Polishd dashboard.
|
|
3
|
+
*
|
|
4
|
+
* // src/app/polishd/page.tsx
|
|
5
|
+
* import { createPolishdPage } from "@polishd/next/dashboard";
|
|
6
|
+
* export { runtime, dynamic, metadata } from "@polishd/next/dashboard";
|
|
7
|
+
* export default createPolishdPage(); // unguarded
|
|
8
|
+
*
|
|
9
|
+
* Protect it by passing an `authenticate` callback (and optionally your own
|
|
10
|
+
* login UI via `unauthorized`):
|
|
11
|
+
*
|
|
12
|
+
* export default createPolishdPage({
|
|
13
|
+
* authenticate: isAuthenticated,
|
|
14
|
+
* unauthorized: <MyLogin />,
|
|
15
|
+
* });
|
|
16
|
+
*/
|
|
17
|
+
import type { Metadata } from "next";
|
|
18
|
+
import type { ReactNode } from "react";
|
|
19
|
+
import { type PolishdDashboardData } from "../server/queries";
|
|
20
|
+
import type { PolishdAISettingsPublic, PolishdProjectProfile, PolishdSummary } from "../ai/types";
|
|
21
|
+
export declare const runtime = "nodejs";
|
|
22
|
+
export declare const dynamic = "force-dynamic";
|
|
23
|
+
export declare const metadata: Metadata;
|
|
24
|
+
export { loadPolishdDashboardData, type PolishdDashboardData } from "../server/queries";
|
|
25
|
+
/** The AI summary card's server-loaded inputs. */
|
|
26
|
+
export interface PolishdAIBundle {
|
|
27
|
+
summary: PolishdSummary | null;
|
|
28
|
+
settings: PolishdAISettingsPublic;
|
|
29
|
+
stale: boolean;
|
|
30
|
+
profile: PolishdProjectProfile | null;
|
|
31
|
+
/** Analytics component identifiers the profile doesn't cover. */
|
|
32
|
+
gaps: string[];
|
|
33
|
+
/** Whether the source tree is on disk here (codebase scan possible). */
|
|
34
|
+
sourceAvailable: boolean;
|
|
35
|
+
}
|
|
36
|
+
export declare function PolishdDashboard({ data, ai, }: {
|
|
37
|
+
data: PolishdDashboardData;
|
|
38
|
+
ai: PolishdAIBundle;
|
|
39
|
+
}): import("react").JSX.Element;
|
|
40
|
+
export interface CreatePolishdPageOptions {
|
|
41
|
+
/**
|
|
42
|
+
* Gate access to the dashboard. Return `true` to allow. If omitted the
|
|
43
|
+
* dashboard renders unguarded (a dev-only console warning is logged).
|
|
44
|
+
*/
|
|
45
|
+
authenticate?: () => boolean | Promise<boolean>;
|
|
46
|
+
/** Rendered when `authenticate` resolves false. Defaults to a minimal screen. */
|
|
47
|
+
unauthorized?: ReactNode;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Build the dashboard page component. Use as the default export of your
|
|
51
|
+
* `app/polishd/page.tsx`.
|
|
52
|
+
*/
|
|
53
|
+
export declare function createPolishdPage(opts?: CreatePolishdPageOptions): () => Promise<import("react").JSX.Element>;
|
|
54
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/dashboard/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AAErC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAEL,KAAK,oBAAoB,EAG1B,MAAM,mBAAmB,CAAC;AAI3B,OAAO,KAAK,EACV,uBAAuB,EACvB,qBAAqB,EACrB,cAAc,EACf,MAAM,aAAa,CAAC;AAOrB,eAAO,MAAM,OAAO,WAAW,CAAC;AAChC,eAAO,MAAM,OAAO,kBAAkB,CAAC;AAEvC,eAAO,MAAM,QAAQ,EAAE,QAGtB,CAAC;AAyLF,OAAO,EAAE,wBAAwB,EAAE,KAAK,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAExF,kDAAkD;AAClD,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,cAAc,GAAG,IAAI,CAAC;IAC/B,QAAQ,EAAE,uBAAuB,CAAC;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACtC,iEAAiE;IACjE,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,wEAAwE;IACxE,eAAe,EAAE,OAAO,CAAC;CAC1B;AAGD,wBAAgB,gBAAgB,CAAC,EAC/B,IAAI,EACJ,EAAE,GACH,EAAE;IACD,IAAI,EAAE,oBAAoB,CAAC;IAC3B,EAAE,EAAE,eAAe,CAAC;CACrB,+BA+QA;AAgBD,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAChD,iFAAiF;IACjF,YAAY,CAAC,EAAE,SAAS,CAAC;CAC1B;AAID;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,GAAE,wBAA6B,8CAsEpE"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { after } from "next/server";
|
|
3
|
+
import { loadPolishdDashboardData, } from "../server/queries";
|
|
4
|
+
import { registerPolishdAuth } from "../ai/guard";
|
|
5
|
+
import { loadProfileState } from "../ai/profile";
|
|
6
|
+
import { generateSummary, getAISettingsPublic, loadSummaryState } from "../ai/summary";
|
|
7
|
+
import ElementsTable from "./elements";
|
|
8
|
+
import TopFeaturesTable from "./features";
|
|
9
|
+
import JourneyList from "./journeys";
|
|
10
|
+
import TopPagesTable from "./pages";
|
|
11
|
+
import SummaryCard from "./summary";
|
|
12
|
+
export const runtime = "nodejs";
|
|
13
|
+
export const dynamic = "force-dynamic";
|
|
14
|
+
export const metadata = {
|
|
15
|
+
title: "Polishd — What gets measured gets improved",
|
|
16
|
+
robots: { index: false, follow: false },
|
|
17
|
+
};
|
|
18
|
+
// ── Design tokens (Vercel aesthetic) ────────────────────────────────────────
|
|
19
|
+
const border = "border-[#2e2e2e]";
|
|
20
|
+
const card = `border ${border} rounded-lg bg-[#0a0a0a]`;
|
|
21
|
+
const label = "text-[11px] font-medium uppercase tracking-[0.08em] text-[#666]";
|
|
22
|
+
const divider = `border-t ${border}`;
|
|
23
|
+
// ── Tooltip ──────────────────────────────────────────────────────────────────
|
|
24
|
+
function InfoTip({ text, anchor = "center", below = false, }) {
|
|
25
|
+
const anchorClass = anchor === "left" ? "left-0" : anchor === "right" ? "right-0" : "left-1/2 -translate-x-1/2";
|
|
26
|
+
const vClass = below ? "top-full mt-2" : "bottom-full mb-2";
|
|
27
|
+
return (_jsxs("span", { className: "group/tip relative ml-1 inline-flex translate-y-px cursor-help align-middle", children: [_jsx("span", { className: "flex h-3.5 w-3.5 items-center justify-center rounded-full border border-[#444] text-[9px] font-bold leading-none text-[#666]", children: "i" }), _jsx("span", { role: "tooltip", className: `pointer-events-none absolute ${vClass} ${anchorClass} z-20 w-64 max-w-[calc(100vw-2rem)] rounded-lg border border-[#2e2e2e] bg-[#111] px-3 py-2 text-left text-[12px] font-normal normal-case leading-snug tracking-normal text-[#aaa] opacity-0 shadow-2xl transition-opacity duration-150 group-hover/tip:opacity-100`, children: text })] }));
|
|
28
|
+
}
|
|
29
|
+
// ── Stat card ────────────────────────────────────────────────────────────────
|
|
30
|
+
function Stat({ label: labelText, value, tip, tone = "text-white", }) {
|
|
31
|
+
return (_jsxs("div", { className: `${card} px-4 py-4`, children: [_jsx("div", { className: `text-[28px] font-semibold tabular-nums leading-none ${tone}`, children: value.toLocaleString() }), _jsxs("div", { className: `mt-2 flex items-center ${label}`, children: [labelText, _jsx(InfoTip, { text: tip })] })] }));
|
|
32
|
+
}
|
|
33
|
+
// ── Table header cell ────────────────────────────────────────────────────────
|
|
34
|
+
function Th({ children, tip, align = "right", }) {
|
|
35
|
+
return (_jsx("th", { className: `whitespace-nowrap py-2.5 ${align === "left" ? "pl-5 pr-6 text-left" : "px-4 text-right"} ${label}`, children: _jsxs("span", { className: "inline-flex items-center gap-0.5", children: [children, _jsx(InfoTip, { text: tip, anchor: align === "left" ? "left" : "right", below: true })] }) }));
|
|
36
|
+
}
|
|
37
|
+
// ── Device row (with usage bar) ──────────────────────────────────────────────
|
|
38
|
+
const DEVICE_META = {
|
|
39
|
+
mobile: { label: "Mobile", icon: "▪" },
|
|
40
|
+
tablet: { label: "Tablet", icon: "▭" },
|
|
41
|
+
desktop: { label: "Desktop", icon: "▭▭" },
|
|
42
|
+
};
|
|
43
|
+
function DeviceRow({ bucket }) {
|
|
44
|
+
const meta = DEVICE_META[bucket.category] ?? { label: bucket.category, icon: "▫" };
|
|
45
|
+
return (_jsxs("div", { className: `flex items-center gap-3 px-4 py-3 sm:px-5 ${divider} first:border-t-0`, children: [_jsx("span", { className: "w-16 shrink-0 text-[13px] font-medium capitalize text-white sm:w-20", children: meta.label }), _jsx("div", { className: "h-2 flex-1 overflow-hidden rounded-full bg-[#1a1a1a]", children: _jsx("div", { className: "h-full rounded-full bg-blue-500", style: { width: `${Math.max(bucket.pct, 1.5)}%` } }) }), _jsxs("span", { className: "w-10 shrink-0 text-right text-[13px] font-semibold tabular-nums text-white sm:w-12", children: [bucket.pct, "%"] }), _jsxs("span", { className: "hidden w-28 shrink-0 text-right text-[12px] tabular-nums text-[#666] sm:block", children: [bucket.sessions, " ", bucket.sessions === 1 ? "session" : "sessions"] }), _jsx("span", { className: "hidden w-20 shrink-0 text-right text-[12px] tabular-nums text-[#555] sm:block", children: bucket.avgWidth === null ? "—" : `~${bucket.avgWidth}px` })] }));
|
|
46
|
+
}
|
|
47
|
+
// ── Section wrapper ──────────────────────────────────────────────────────────
|
|
48
|
+
function Section({ title, children }) {
|
|
49
|
+
return (_jsxs("section", { className: "mb-8", children: [_jsx("div", { className: `mb-3 flex items-center gap-2 ${label}`, children: title }), children] }));
|
|
50
|
+
}
|
|
51
|
+
// ── Monitored components helpers ─────────────────────────────────────────────
|
|
52
|
+
function fmtMs(ms) {
|
|
53
|
+
if (ms < 1000)
|
|
54
|
+
return `${ms}ms`;
|
|
55
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
56
|
+
}
|
|
57
|
+
function MonitoredRow({ m }) {
|
|
58
|
+
const hasIssues = m.rageClicks > 0 || m.deadClicks > 0;
|
|
59
|
+
return (_jsxs("tr", { className: `${divider} first:border-t-0 align-top`, children: [_jsxs("td", { className: "py-2.5 pl-5 pr-4 min-w-[160px]", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [_jsx("span", { className: "text-[13px] font-medium text-white", children: m.name }), _jsx("span", { className: "rounded px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wider bg-purple-950 text-purple-400", children: "monitored" }), hasIssues && (_jsx("span", { className: "rounded px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wider bg-red-950 text-red-400", children: "issues" }))] }), _jsxs("div", { className: "mt-0.5 text-[11px] text-[#555]", children: [m.sessions, " ", m.sessions === 1 ? "session" : "sessions", " \u00B7 ", m.pages, " ", m.pages === 1 ? "page" : "pages"] })] }), _jsx("td", { className: "py-2.5 px-4 text-right text-[13px] tabular-nums text-[#888]", children: m.mounts > 0 ? m.mounts : _jsx("span", { className: "text-[#444]", children: "\u2014" }) }), _jsx("td", { className: "py-2.5 px-4 text-right text-[13px] font-semibold tabular-nums text-white", children: m.componentViews > 0 ? m.componentViews : _jsx("span", { className: "font-normal text-[#444]", children: "\u2014" }) }), _jsx("td", { className: "py-2.5 px-4 text-right text-[13px] tabular-nums text-[#888]", children: m.avgViewMs !== null ? fmtMs(m.avgViewMs) : _jsx("span", { className: "text-[#444]", children: "\u2014" }) }), _jsx("td", { className: "py-2.5 px-4 text-right text-[13px] tabular-nums text-[#888]", children: m.avgScrollDepth !== null ? `${m.avgScrollDepth}%` : _jsx("span", { className: "text-[#444]", children: "\u2014" }) }), _jsx("td", { className: "py-2.5 px-4 text-right text-[13px] tabular-nums text-[#555]", children: m.heightPx !== null ? `${m.heightPx}px` : _jsx("span", { className: "text-[#444]", children: "\u2014" }) }), _jsx("td", { className: "py-2.5 px-4 text-right text-[13px] tabular-nums text-[#888]", children: m.clicks > 0 ? m.clicks : _jsx("span", { className: "text-[#444]", children: "\u2014" }) }), _jsx("td", { className: "py-2.5 px-4 text-right text-[13px] tabular-nums", children: m.hovers > 0 ? _jsx("span", { className: "text-[#888]", children: m.hovers }) : _jsx("span", { className: "text-[#444]", children: "\u2014" }) }), _jsx("td", { className: "py-2.5 pl-4 pr-5 text-right text-[13px] tabular-nums", children: m.rageClicks > 0 ? _jsx("span", { className: "text-red-400", children: m.rageClicks }) : _jsx("span", { className: "text-[#444]", children: "\u2014" }) })] }));
|
|
60
|
+
}
|
|
61
|
+
// ── Data loading ─────────────────────────────────────────────────────────────
|
|
62
|
+
// `loadPolishdDashboardData` + `PolishdDashboardData` now live in server/queries so
|
|
63
|
+
// the AI summary layer can share them without importing React; re-export for
|
|
64
|
+
// any existing callers of this module.
|
|
65
|
+
export { loadPolishdDashboardData } from "../server/queries";
|
|
66
|
+
// ── Dashboard (presentational) ───────────────────────────────────────────────
|
|
67
|
+
export function PolishdDashboard({ data, ai, }) {
|
|
68
|
+
const { overview, pages, elements, devices, topUsed, journeys, errors, monitored } = data;
|
|
69
|
+
return (_jsxs("main", { className: "mx-auto max-w-5xl px-4 py-8 sm:px-6 sm:py-10 text-white", children: [_jsxs("div", { className: `mb-8 flex items-start justify-between gap-3 border-b ${border} pb-6`, children: [_jsxs("div", { children: [_jsx("p", { className: label, children: "Polishd" }), _jsx("h1", { className: "mt-1 text-[22px] font-semibold tracking-tight text-white", children: "What gets measured gets improved" }), _jsxs("p", { className: "mt-1 text-[13px] text-[#666]", children: ["Stage 1 \u2014 Capture. Hover", " ", _jsx("span", { className: "inline-flex h-3 w-3 items-center justify-center rounded-full border border-[#444] text-[8px] font-bold text-[#666]", children: "i" }), " ", "for calculation details."] })] }), _jsx("div", { className: `shrink-0 whitespace-nowrap rounded-full px-3 py-1 text-[11px] font-medium ${overview.ready
|
|
70
|
+
? "bg-emerald-950 text-emerald-400"
|
|
71
|
+
: "bg-[#1a1a1a] text-[#666]"}`, children: overview.ready ? "● collecting" : "○ no store" })] }), !overview.ready && (_jsxs("div", { className: `mb-8 rounded-lg border border-amber-900/50 bg-amber-950/30 px-4 py-3 text-[13px] text-amber-400`, children: ["The analytics store isn't writable in this environment. Run locally or configure a database \u2014 see the ", _jsx("span", { className: "font-mono text-amber-300", children: "@polishd/next" }), " DATABASE.md guide."] })), _jsx(SummaryCard, { initialSummary: ai.summary, initialSettings: ai.settings, initialStale: ai.stale, initialProfile: ai.profile, initialGaps: ai.gaps, sourceAvailable: ai.sourceAvailable }), _jsx(Section, { title: "Overview", children: _jsxs("div", { className: "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6", children: [_jsx(Stat, { label: "Sessions", value: overview.sessions, tip: "Distinct anonymous visitors, counted by the polishd_session cookie. One cookie = one session; no fingerprinting." }), _jsx(Stat, { label: "Page views", value: overview.pageViews, tip: "page_view events \u2014 initial page loads plus client-side (soft) navigations between routes." }), _jsx(Stat, { label: "Rage clicks", value: overview.rageClicks, tone: "text-red-500", tip: "3+ clicks on the same element within 500ms. A strong signal of frustration \u2014 something looks clickable or is broken." }), _jsx(Stat, { label: "Dead clicks", value: overview.deadClicks, tone: "text-[#f5a623]", tip: "Clicks on non-interactive elements (no link, button, or role within 4 ancestors). Users expected something to happen but nothing did." }), _jsx(Stat, { label: "JS errors", value: overview.jsErrors, tone: "text-red-500", tip: "Uncaught exceptions and unhandled promise rejections, with page and component context." }), _jsx(Stat, { label: "Events", value: overview.totalEvents, tip: "Total raw signals captured across all event types." })] }) }), _jsx(Section, { title: _jsxs(_Fragment, { children: ["Device sizes", _jsx(InfoTip, { anchor: "left", text: "One viewport sample per session, taken at session start, bucketed by CSS width: mobile (<640px), tablet (640\u20131023px), desktop (\u22651024px). Shows what screen size visitors actually use." })] }), children: _jsx("div", { className: card, children: devices.length === 0 ? (_jsx("p", { className: "px-5 py-8 text-center text-[13px] text-[#555]", children: "No viewport data yet \u2014 browse the site then refresh." })) : (devices.map((d) => _jsx(DeviceRow, { bucket: d }, d.category))) }) }), _jsx(Section, { title: _jsxs(_Fragment, { children: ["Top pages", _jsx(InfoTip, { anchor: "left", text: "The most-visited routes, ranked by distinct sessions. Click a page to open a chart of its sessions over time." })] }), children: _jsx(TopPagesTable, { pages: pages }) }), _jsx(Section, { title: _jsxs(_Fragment, { children: ["Monitored components", _jsx(InfoTip, { anchor: "left", text: "Components explicitly wrapped in <PolishdMonitor>. Shows viewport engagement (views, avg time visible, scroll depth, dimensions) alongside click activity. A high view count with low clicks often signals interest without commitment." })] }), children: _jsx("div", { className: card, children: monitored.length === 0 ? (_jsxs("p", { className: "px-5 py-8 text-center text-[13px] text-[#555]", children: ["No monitored components yet \u2014 wrap elements with", " ", _jsx("code", { className: "font-mono text-[#777]", children: "<PolishdMonitor name=\"…\">" }), " to track them here."] })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full min-w-[700px]", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx(Th, { align: "left", tip: "The name prop passed to <PolishdMonitor>. Sessions and pages shown beneath.", children: "Component" }), _jsx(Th, { tip: "Times this region rendered (mount events). Only content-tracked monitors emit these.", children: "Mounts" }), _jsx(Th, { tip: "Times this component entered the viewport for \u2265500ms.", children: "Views" }), _jsx(Th, { tip: "Average time visible per viewport visit \u2014 a proxy for reading/engagement time.", children: "Avg time" }), _jsx(Th, { tip: "Average % of the component's height scrolled through per visit. 100% = user reached the bottom.", children: "Scroll depth" }), _jsx(Th, { tip: "Largest rendered height in px seen for this component \u2014 for an article, its full content height.", children: "Height" }), _jsx(Th, { tip: "Normal (non-rage, non-dead) clicks.", children: "Clicks" }), _jsx(Th, { tip: "Deliberate pointer hovers (\u2265200ms dwell).", children: "Hovers" }), _jsx(Th, { tip: "Rage clicks (3+ rapid clicks) \u2014 frustration signal.", children: "Rage" })] }) }), _jsx("tbody", { children: monitored.map((m) => (_jsx(MonitoredRow, { m: m }, m.name))) })] }) })) }) }), _jsx(Section, { title: _jsxs(_Fragment, { children: ["Sampled user journeys", _jsx(InfoTip, { anchor: "left", text: "Full sessions sampled and ranked by a composite score (rage\u00D73 + dead\u00D72 + errors\u00D72.5), preferring complete recordings and recent ones. Click a session to open its start-to-finish flow chart." })] }), children: _jsx(JourneyList, { journeys: journeys }) }), _jsx(Section, { title: _jsxs(_Fragment, { children: ["Most-used features", _jsx(InfoTip, { anchor: "left", text: "Interactive elements ranked by raw click volume (successful clicks only \u2014 rage and dead clicks excluded). The buttons and features visitors actually use most." })] }), children: _jsx("div", { className: card, children: topUsed.length === 0 ? (_jsx("p", { className: "px-5 py-8 text-center text-[13px] text-[#555]", children: "No clicks captured yet \u2014 browse the site then refresh." })) : (_jsx(TopFeaturesTable, { features: topUsed, header: _jsxs("tr", { children: [_jsx(Th, { align: "left", tip: "Component name (from data-component) or DOM selector. Sample text and selector shown beneath.", children: "Element" }), _jsx(Th, { tip: "Total successful (interactive) clicks across all pages.", children: "Clicks" }), _jsx(Th, { tip: "Distinct sessions that clicked this element.", children: "Sessions" }), _jsx(Th, { tip: "How many distinct pages this element was clicked on.", children: "Pages" })] }) })) }) }), _jsx(Section, { title: _jsxs(_Fragment, { children: ["Interactions by element", _jsx(InfoTip, { anchor: "left", text: "Click-type events grouped by DOM selector. This is which UI element the issues are on \u2014 the primary input to Stage 2 synthesis. Components wrapped in <PolishdMonitor> are listed separately under Monitored components." })] }), children: _jsx("div", { className: card, children: elements.length === 0 ? (_jsx("p", { className: "px-5 py-8 text-center text-[13px] text-[#555]", children: "No interactions captured yet \u2014 browse the site then refresh." })) : (_jsx(ElementsTable, { elements: elements, header: _jsxs("tr", { children: [_jsx(Th, { align: "left", tip: "DOM selector path for the interacted element. Sample text shown beneath.", children: "Element" }), _jsx(Th, { tip: "rage\u00D73 + dead\u00D72 for this element across all pages.", children: "Score" }), _jsx(Th, { tip: "Normal (non-rage, non-dead) clicks.", children: "Clicks" }), _jsx(Th, { tip: "Rage clicks on this element.", children: "Rage" }), _jsx(Th, { tip: "Dead clicks on this element.", children: "Dead" }), _jsx(Th, { tip: "How many distinct pages this element appeared on.", children: "Pages" })] }) })) }) }), errors.length > 0 && (_jsx(Section, { title: "Recent errors", children: _jsx("div", { className: card, children: errors.map((e, i) => (_jsx("div", { className: i > 0 ? divider : "", children: _jsxs("div", { className: "px-5 py-3", children: [_jsx("div", { className: "font-mono text-[13px] text-red-400", children: e.message }), _jsxs("div", { className: "mt-0.5 text-[11px] text-[#555]", children: [e.path, e.component ? ` · ${e.component}` : ""] })] }) }, i))) }) }))] }));
|
|
72
|
+
}
|
|
73
|
+
// ── Built-in fallback for a failed auth check ────────────────────────────────
|
|
74
|
+
function Unauthorized() {
|
|
75
|
+
return (_jsxs("main", { className: "mx-auto flex min-h-[60vh] max-w-md flex-col items-center justify-center px-6 text-center text-white", children: [_jsx("p", { className: label, children: "Polishd" }), _jsx("h1", { className: "mt-2 text-[20px] font-semibold tracking-tight", children: "Unauthorized" }), _jsxs("p", { className: "mt-2 text-[13px] text-[#666]", children: ["This dashboard is protected. Pass an ", _jsx("code", { className: "font-mono", children: "unauthorized" }), " element to", _jsx("code", { className: "font-mono", children: " createPolishdPage()" }), " to render your own sign-in UI here."] })] }));
|
|
76
|
+
}
|
|
77
|
+
let warnedUnguarded = false;
|
|
78
|
+
/**
|
|
79
|
+
* Build the dashboard page component. Use as the default export of your
|
|
80
|
+
* `app/polishd/page.tsx`.
|
|
81
|
+
*/
|
|
82
|
+
export function createPolishdPage(opts = {}) {
|
|
83
|
+
// Register the policy at module evaluation, not per render: the AI server
|
|
84
|
+
// actions are reachable without this page ever rendering, and they re-run
|
|
85
|
+
// `authenticate` themselves before doing any work. See ai/guard.ts.
|
|
86
|
+
registerPolishdAuth(opts.authenticate
|
|
87
|
+
? { mode: "guarded", authenticate: opts.authenticate }
|
|
88
|
+
: { mode: "open" });
|
|
89
|
+
return async function PolishdPage() {
|
|
90
|
+
if (opts.authenticate) {
|
|
91
|
+
const ok = await opts.authenticate();
|
|
92
|
+
if (!ok)
|
|
93
|
+
return _jsx(_Fragment, { children: opts.unauthorized ?? _jsx(Unauthorized, {}) });
|
|
94
|
+
}
|
|
95
|
+
else if (!warnedUnguarded && process.env.NODE_ENV !== "production") {
|
|
96
|
+
warnedUnguarded = true;
|
|
97
|
+
console.warn("[polishd] dashboard is unguarded — pass `authenticate` to createPolishdPage() to protect it.");
|
|
98
|
+
}
|
|
99
|
+
const data = await loadPolishdDashboardData();
|
|
100
|
+
const [summaryState, settings, profileState] = await Promise.all([
|
|
101
|
+
loadSummaryState(data),
|
|
102
|
+
getAISettingsPublic(),
|
|
103
|
+
loadProfileState(data),
|
|
104
|
+
]);
|
|
105
|
+
// Cadence-based auto-refresh: when the owner opted into daily/weekly and
|
|
106
|
+
// the summary is both past its cadence AND built from different data,
|
|
107
|
+
// regenerate in the background after this response is sent. Unchanged data
|
|
108
|
+
// never triggers a model call, so the cadence only spends tokens when
|
|
109
|
+
// there's genuinely new behavior to narrate.
|
|
110
|
+
const cadence = settings.refreshCadence ?? "manual";
|
|
111
|
+
if (cadence !== "manual" &&
|
|
112
|
+
settings.hasApiKey &&
|
|
113
|
+
data.overview.ready &&
|
|
114
|
+
data.overview.totalEvents > 0) {
|
|
115
|
+
const cadenceMs = cadence === "daily" ? 86_400_000 : 604_800_000;
|
|
116
|
+
const due = summaryState.summary === null ||
|
|
117
|
+
(summaryState.stale &&
|
|
118
|
+
Date.now() - summaryState.summary.generatedAt > cadenceMs);
|
|
119
|
+
if (due) {
|
|
120
|
+
after(async () => {
|
|
121
|
+
try {
|
|
122
|
+
await generateSummary();
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
console.warn("[polishd] scheduled summary refresh failed:", err);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return (_jsx(PolishdDashboard, { data: data, ai: {
|
|
131
|
+
summary: summaryState.summary,
|
|
132
|
+
settings,
|
|
133
|
+
stale: summaryState.stale,
|
|
134
|
+
profile: profileState.profile,
|
|
135
|
+
gaps: profileState.gaps,
|
|
136
|
+
sourceAvailable: profileState.sourceAvailable,
|
|
137
|
+
} }));
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=index.js.map
|