Skip to content

Commit 4b10442

Browse files
authored
Merge branch 'refactor/data-grid-and-dashboard-surfaces' into refactor/assistant-ui-chat-surfaces
2 parents f32b206 + 9e09c82 commit 4b10442

49 files changed

Lines changed: 1409 additions & 1669 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/CLAUDE-KNOWLEDGE.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,9 @@ Then restart the dev server. This rebuilds all packages and generates the necess
359359
## Q: How is backwards compatibility for the offer→product rename handled in the payments purchase APIs?
360360
A: API v1 requests are routed through the `v2beta1` migration. The migration wraps the latest handlers, accepts legacy `offer_id`/`offer_inline` request fields, translates product-related errors back to the old offer error codes/messages, and augments responses (like `validate-code`) with `offer`/`conflicting_group_offers` aliases alongside the new `product` fields. Newer API versions keep the product-only contract.
361361

362+
## Q: How does the Stack Auth template dev tool decide whether to iframe the dashboard?
363+
A: The dev tool always iframes the Dashboard tab and provides an "Open in New Tab" escape hatch for auth or framing issues.
364+
362365
### Q: What's the reliable way to run targeted tests across backend, dashboard, stack-shared, and e2e at once?
363366
A: Run from the monorepo root with explicit file paths: `pnpm test run "<path1>" "<path2>" ...`. This works even when individual packages do not define a local `test` script. Also avoid passing an extra `run` argument to package-level `test` scripts that already execute `vitest run`.
364367

@@ -395,6 +398,54 @@ A: The `/api/v1/internal/metrics` response now intentionally includes `analytics
395398
## Q: Why can environment config override writes fail with a product/product-line customer type warning after creating a preview project?
396399
A: The environment override endpoint validates the new environment override against the rendered branch config. Preview dummy payments data must therefore be internally coherent: products assigned to a product line need the same `customerType` as that product line, otherwise unrelated environment patches can fail with warnings like `Product "growth" has customer type "user" but its product line "workspace" has customer type "team"`.
397400

401+
## Q: How do you keep the Stack Auth dev tool from reopening automatically after navigation or reload?
402+
A: Treat `isOpen` as mount-local state in `packages/template/src/dev-tool/dev-tool-core.ts`: load persisted preferences with `isOpen: false`, and save state back to localStorage with `isOpen: false` so tab/size preferences persist without reopening the panel on the next mount.
403+
404+
## Q: How should the Stack Auth dev tool indicator avoid being hidden by other dev indicators?
405+
A: Do not dynamically reflow around framework indicators; that makes pointer interaction brittle. Keep the trigger anchored to its saved corner and give `.sdt-trigger` a max practical z-index (`2147483647`) so the Stack indicator renders above Next/Turbo overlays.
406+
407+
## Q: How should Stack Auth dev tool trigger movement feel?
408+
A: Dragging should remain instant/direct, but programmatic moves like snap-to-corner after drag, resize reposition, and post-measurement correction should use a short snappy left/top transition. In `dev-tool-core.ts`, toggle a dedicated animation class only for those programmatic updates and remove it shortly after.
409+
410+
## Q: How do you prevent duplicate Stack Auth dev tool indicators from multiple package/module instances?
411+
A: `createDevTool` in `packages/template/src/dev-tool/dev-tool-core.ts` should register a browser-wide singleton instance on `window` with an idempotent cleanup function, call any previous global cleanup before mounting, and remove leftover `#__stack-dev-tool-root` nodes as a fallback for older instances that did not register cleanup.
412+
413+
## Q: How should the Stack Auth dev tool handle Dashboard tab sizing?
414+
A: Keep the user's default panel width/height in state for normal tabs, but apply a transient fullscreen class while the active tab is `dashboard`. The fullscreen class should override fixed dimensions and hide resize handles, then remove itself and restore the saved default dimensions when any other tab is selected.
415+
416+
## Q: How should the Stack Auth dev tool animate Dashboard fullscreen transitions?
417+
A: Add a short-lived geometry animation class only around tab-driven switches into or out of Dashboard fullscreen. Animate `width`, `height`, `right`, `bottom`, and radius for the mode change, then remove the class so manual dragging/resizing remains direct and does not lag.
418+
419+
## Q: Should the Stack Auth dev tool Dashboard tab be gated on local emulator mode?
420+
A: No. The Dashboard tab should always render the dashboard URL in an iframe inside the dev tool, and should also show an "Open in New Tab" link so users can escape iframe/auth/framing issues without losing the embedded view.
421+
422+
## Q: How should the Dashboard iframe use space in the Stack Auth dev tool?
423+
A: In Dashboard fullscreen mode, the panel should cover the full viewport with no inset or rounded frame, and the iframe should fill all available content space. Put auxiliary actions like "Open in New Tab" in a top-edge overlay below the tab bar so they do not reserve layout height from the iframe.
424+
425+
## Q: How do you maximize iframe tabs inside the Stack Auth dev tool panel?
426+
A: Mark Docs/Dashboard panes with an iframe-specific class, remove the normal 16px tab-pane padding, hide pane overflow, and give the iframe container explicit `width: 100%` and `height: 100%`. Keep toolbar actions as absolute overlays so they do not reduce iframe layout space.
427+
428+
## Q: How should docs access work in the Stack Auth dev tool?
429+
A: Docs should not be an iframe-backed tab. Keep docs as a top-bar external link to `https://docs.stack-auth.com` with an up-right arrow icon, and migrate any persisted `activeTab: "docs"` value back to `overview`.
430+
431+
## Q: How should the Stack Auth dev tool Console tab handle large log volumes?
432+
A: Keep the full log history in the shared log store, but render only the newest 100 entries initially. When the log scroll area nears the bottom, increase the visible count by another 100 and rerender. The Console tab should be a single logs view with Copy, Export, and Clear buttons in the header rather than nested Logs/Config subtabs.
433+
434+
## Q: How should the Stack Auth dev tool Customize page detail show page metadata?
435+
A: Avoid repeated page-type badges like `Handler` in page tiles or detail headers. Keep actionable badges such as `Outdated`, show the compact route path next to the page title, use `Open` for the page action, and phrase the customization prompt as "Want to customize this page? Paste this prompt into your coding agent." with a `Copy prompt` button below it.
436+
437+
## Q: How should the Stack Auth dev tool Customize page Open action behave?
438+
A: The page detail `Open` action should be a taller button matching the redirect code chip height, include a small up-right arrow icon, and always open the selected page in a new tab.
439+
440+
## Q: How should the Stack Auth dev tool Support tab be structured?
441+
A: Do not keep top-level Feedback/Feature Request subtabs unless the backend supports them. The Support tab should mount the feedback form directly, show Discord/Email/GitHub links at the top, keep only Feedback and Bug Report choices in the form, and use a Submit button with a right-arrow icon after the text.
442+
443+
## Q: Which tab should the Stack Auth dev tool show when opened?
444+
A: Opening the dev tool should always reset `activeTab` to `overview` before creating the panel. Other preferences like size can persist, but each fresh open should start on Overview instead of the last-used tab.
445+
446+
## Q: How should Stack Auth dev tool PR review comments around Overview and trigger behavior be handled?
447+
A: Keep trigger corner resolution on-screen even in tiny viewports by snapping to bounded edge positions, remove unused trigger-position helpers, treat browser fetch `TypeError`s such as Safari's `Load failed` as best-effort Overview hydration errors, replace auth-method skeletons with a fallback on every load failure, and compute the `Auth method active` checklist row from loaded project config instead of hard-coding it as passing.
448+
398449
## Q: Why can `pnpm run dev` fail with `ERR_MODULE_NOT_FOUND` for `@stackframe/stack/dist/esm/index.js` during OpenAPI docs generation?
399450
A: Root `dev` starts the OpenAPI docs watcher at the same time as package `dev` watchers. If a package `dev` script removes `dist` before `tsdown --watch` recreates it, the docs generator can import `apps/backend/src/stack.tsx` while `@stackframe/stack`'s ESM entrypoint is temporarily missing. Package watch scripts should update `dist` in place, and eager generators should wait for package imports to resolve before running.
400451

.github/workflows/claude.yml

Lines changed: 0 additions & 64 deletions
This file was deleted.

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ To see all development ports, refer to the index.html of `apps/dev-launchpad/pub
112112
- Always let me know about the tradeoffs and decisions you make while implementing a non-trivial change.
113113
- Whenever you change the URL of a page in the docs (or remove one), add a redirect in the docs-mintlify/docs.json file to make sure we don't lose any SEO juice.
114114
- When you made frontend (or docs, dashboard, demo, etc.) changes, and you have a browser MCP in your list of MCP tools, make sure to test the changes in the browser MCP.
115+
- If you're using the browser to test the dashboard and need to sign in, use GitHub OAuth to sign in (by default it should redirect you to the mock OAuth provider page, where you can sign in with admin@example.com).
116+
- NEVER INSTALL A NEW PACKAGE (or anything else) WITHOUT EXPLICIT APPROVAL FROM THE USER.
115117

116118
### Code-related
117119
- Use ES6 maps instead of records wherever you can.

apps/backend/prisma/migrations/20260420000000_add_project_onboarding_state/tests/default-and-updates.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export const postMigration = async (sql: Sql, ctx: Awaited<ReturnType<typeof pre
2929
};
3030
await sql`
3131
UPDATE "Project"
32-
SET "onboardingState" = ${JSON.stringify(onboardingState)}::jsonb
32+
SET "onboardingState" = ${sql.json(onboardingState)}::jsonb
3333
WHERE "id" = ${ctx.projectId}
3434
`;
3535

apps/backend/src/app/api/latest/internal/projects-weekly-users/route.tsx

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { KnownErrors } from "@stackframe/stack-shared";
77
import { MetricsDataPointsSchema } from "@stackframe/stack-shared/dist/interface/admin-metrics";
88
import { adaptSchema, clientOrHigherAuthTypeSchema, yupNumber, yupObject, yupRecord, yupString } from "@stackframe/stack-shared/dist/schema-fields";
99
import { StackAssertionError, captureError } from "@stackframe/stack-shared/dist/utils/errors";
10-
const WINDOW_DAYS = 7;
10+
const WEEKLY_USERS_WINDOW_DAYS = 7;
11+
const CHART_WINDOW_DAYS = 30;
1112
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
1213

1314
type ProjectWeeklyUsers = {
@@ -19,8 +20,8 @@ export function applyProjectWeeklyUsersRows(
1920
byProject: Map<string, ProjectWeeklyUsers>,
2021
rows: { projectId: string, day: string, users: number }[],
2122
) {
22-
// GROUPING SETS emits one rollup row per project with day defaulted to the
23-
// ClickHouse Date epoch ("1970-01-01"); those rows hold the weekly total.
23+
// The query emits one synthetic row per project with day set to the ClickHouse
24+
// Date epoch ("1970-01-01"); those rows hold the weekly total.
2425
const dailyIndex = new Map<string, Map<string, number>>();
2526
for (const row of rows) {
2627
const project = byProject.get(row.projectId);
@@ -83,12 +84,13 @@ export const GET = createSmartRouteHandler({
8384
const now = new Date();
8485
const todayUtc = new Date(now);
8586
todayUtc.setUTCHours(0, 0, 0, 0);
86-
const since = new Date(todayUtc.getTime() - (WINDOW_DAYS - 1) * ONE_DAY_MS);
87+
const since = new Date(todayUtc.getTime() - (CHART_WINDOW_DAYS - 1) * ONE_DAY_MS);
88+
const weeklySince = new Date(todayUtc.getTime() - (WEEKLY_USERS_WINDOW_DAYS - 1) * ONE_DAY_MS);
8789
const untilExclusive = new Date(todayUtc.getTime() + ONE_DAY_MS);
8890

8991
const emptySeries = () => {
9092
const out: { date: string, activity: number }[] = [];
91-
for (let i = 0; i < WINDOW_DAYS; i += 1) {
93+
for (let i = 0; i < CHART_WINDOW_DAYS; i += 1) {
9294
const day = new Date(since.getTime() + i * ONE_DAY_MS);
9395
out.push({ date: day.toISOString().split("T")[0], activity: 0 });
9496
}
@@ -117,6 +119,7 @@ export const GET = createSmartRouteHandler({
117119
projectIds,
118120
branchId: DEFAULT_BRANCH_ID,
119121
since: since.toISOString().slice(0, 19),
122+
weeklySince: weeklySince.toISOString().slice(0, 19),
120123
untilExclusive: untilExclusive.toISOString().slice(0, 19),
121124
};
122125

@@ -126,7 +129,7 @@ export const GET = createSmartRouteHandler({
126129
query: `
127130
SELECT
128131
project_id AS projectId,
129-
toDate(event_at, 'UTC') AS day,
132+
toString(toDate(event_at, 'UTC')) AS day,
130133
uniqExact(assumeNotNull(user_id)) AS users
131134
FROM analytics_internal.events
132135
WHERE event_type = '$token-refresh'
@@ -136,7 +139,21 @@ export const GET = createSmartRouteHandler({
136139
AND event_at >= {since:DateTime}
137140
AND event_at < {untilExclusive:DateTime}
138141
AND coalesce(CAST(data.is_anonymous, 'Nullable(UInt8)'), 0) = 0
139-
GROUP BY GROUPING SETS ((projectId), (projectId, day))
142+
GROUP BY projectId, day
143+
UNION ALL
144+
SELECT
145+
project_id AS projectId,
146+
'1970-01-01' AS day,
147+
uniqExact(assumeNotNull(user_id)) AS users
148+
FROM analytics_internal.events
149+
WHERE event_type = '$token-refresh'
150+
AND project_id IN {projectIds:Array(String)}
151+
AND branch_id = {branchId:String}
152+
AND user_id IS NOT NULL
153+
AND event_at >= {weeklySince:DateTime}
154+
AND event_at < {untilExclusive:DateTime}
155+
AND coalesce(CAST(data.is_anonymous, 'Nullable(UInt8)'), 0) = 0
156+
GROUP BY projectId
140157
`,
141158
query_params: queryParams,
142159
format: "JSONEachRow",

apps/backend/src/app/api/latest/internal/session-replays/route.tsx

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,6 @@ function parseCsvUuids(name: string, raw: string | undefined): string[] {
3131
return values;
3232
}
3333

34-
function escapeLikePattern(input: string): string {
35-
return input.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
36-
}
37-
3834
function parseNonNegativeInt(name: string, raw: string | undefined): number | null {
3935
if (!raw) return null;
4036
const value = Number(raw);
@@ -104,8 +100,6 @@ export const GET = createSmartRouteHandler({
104100
last_event_at_from_millis: yupString().optional(),
105101
last_event_at_to_millis: yupString().optional(),
106102
click_count_min: yupString().optional(),
107-
sort_direction: yupString().oneOf(["asc", "desc"]).optional(),
108-
q: yupString().optional(),
109103
}).optional(),
110104
}),
111105
response: yupObject({
@@ -144,8 +138,6 @@ export const GET = createSmartRouteHandler({
144138
const clickCountMin = parseNonNegativeInt("click_count_min", query.click_count_min);
145139
const lastEventAtFrom = parseMillis("last_event_at_from_millis", query.last_event_at_from_millis);
146140
const lastEventAtTo = parseMillis("last_event_at_to_millis", query.last_event_at_to_millis);
147-
const sortDirection: "asc" | "desc" = query.sort_direction === "asc" ? "asc" : "desc";
148-
const searchQuery = query.q?.trim() || null;
149141

150142
if (durationMsMin !== null && durationMsMax !== null && durationMsMin > durationMsMax) {
151143
throw new StatusError(StatusError.BadRequest, "duration_ms_min must be less than or equal to duration_ms_max");
@@ -226,22 +218,11 @@ export const GET = createSmartRouteHandler({
226218
${clickQualifiedIds ? Prisma.sql`AND sr."id" IN (${Prisma.join(clickQualifiedIds)})` : Prisma.empty}
227219
${durationMsMin !== null ? Prisma.sql`AND EXTRACT(EPOCH FROM (sr."lastEventAt" - sr."startedAt")) * 1000 >= ${durationMsMin}` : Prisma.empty}
228220
${durationMsMax !== null ? Prisma.sql`AND EXTRACT(EPOCH FROM (sr."lastEventAt" - sr."startedAt")) * 1000 <= ${durationMsMax}` : Prisma.empty}
229-
${searchQuery ? Prisma.sql`AND (
230-
sr."id"::text ILIKE ${`%${escapeLikePattern(searchQuery)}%`}
231-
OR pu."displayName" ILIKE ${`%${escapeLikePattern(searchQuery)}%`}
232-
)` : Prisma.empty}
233-
${cursorPivot ? (sortDirection === "asc"
234-
? Prisma.sql`AND (
235-
sr."lastEventAt" > ${cursorPivot.lastEventAt}
236-
OR (sr."lastEventAt" = ${cursorPivot.lastEventAt} AND sr."id" > ${cursorId})
237-
)`
238-
: Prisma.sql`AND (
239-
sr."lastEventAt" < ${cursorPivot.lastEventAt}
240-
OR (sr."lastEventAt" = ${cursorPivot.lastEventAt} AND sr."id" < ${cursorId})
241-
)`) : Prisma.empty}
242-
${sortDirection === "asc"
243-
? Prisma.sql`ORDER BY sr."lastEventAt" ASC, sr."id" ASC`
244-
: Prisma.sql`ORDER BY sr."lastEventAt" DESC, sr."id" DESC`}
221+
${cursorPivot ? Prisma.sql`AND (
222+
sr."lastEventAt" < ${cursorPivot.lastEventAt}
223+
OR (sr."lastEventAt" = ${cursorPivot.lastEventAt} AND sr."id" < ${cursorId})
224+
)` : Prisma.empty}
225+
ORDER BY sr."lastEventAt" DESC, sr."id" DESC
245226
LIMIT ${limit + 1}
246227
`;
247228

0 commit comments

Comments
 (0)