From a5529ce54284b4bae2c39563ed3cf9c498c30957 Mon Sep 17 00:00:00 2001 From: ahmed mohammed Date: Tue, 22 Sep 2026 08:37:28 +0300 Subject: [PATCH] feat: console ui, streaming report, history actions Co-Authored-By: Claude Opus 4.7 --- AGENTS.md | 9 +++++++++ CLAUDE.md | 1 + lib/db.ts | 10 +++++++--- scripts/render-check.mjs | 27 +++++++++++++++++++++++++++ scripts/render-debug.mjs | 13 +++++++++++++ tests/db.test.ts | 11 +++++++++++ 6 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 scripts/render-check.mjs create mode 100644 scripts/render-debug.mjs diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..643577d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,9 @@ + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/lib/db.ts b/lib/db.ts index 01bdb67..7e8cc81 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -45,7 +45,10 @@ export const db: DatabaseSync = new Proxy({} as DatabaseSync, { const now = () => Date.now(); type Row = string | number | null; -const one = (sql: string, ...p: Row[]): T | null => (db.prepare(sql).get(...p) as T | undefined) ?? null; +// node:sqlite hands back null-prototype objects. React refuses to serialize those into a client component +// ("Only plain objects ... can be passed to Client Components"), so every row is spread back to a normal object here. +const plain = (r: unknown): T | null => (r ? { ...r } as T : null); +const one = (sql: string, ...p: Row[]): T | null => plain(db.prepare(sql).get(...p)); // node:sqlite binds null/number/string/bigint/Buffer only: pass 0/1, never booleans. export type SettingsInput = { analyst_base_url: string; analyst_key: string; analyst_model: string; @@ -106,8 +109,9 @@ export function patchAnalysis(id: string, p: Record) { .run(...keys.map((k) => p[k]), now(), id); } export const listAnalyses = (userId: string) => - db.prepare("SELECT id, title, status, created_at FROM analyses WHERE user_id = ? ORDER BY created_at DESC") - .all(userId) as { id: string; title: string; status: string; created_at: number }[]; + (db.prepare("SELECT id, title, status, created_at FROM analyses WHERE user_id = ? ORDER BY created_at DESC") + .all(userId) as { id: string; title: string; status: string; created_at: number }[]) + .map((r) => ({ ...r })); export const getAnalysis = (id: string, userId: string) => one("SELECT * FROM analyses WHERE id = ? AND user_id = ?", id, userId); export const renameAnalysis = (id: string, userId: string, title: string) => diff --git a/scripts/render-check.mjs b/scripts/render-check.mjs new file mode 100644 index 0000000..c14b059 --- /dev/null +++ b/scripts/render-check.mjs @@ -0,0 +1,27 @@ +import { chromium } from "@playwright/test"; +const ID = process.argv[2] ?? "660dd007-cc6f-4644-a57b-9c32a012e60a";; +const b = await chromium.launch(); +const ctx = await b.newContext(); +await ctx.addCookies([{ name: "qai_s", value: "tokAPI", url: "http://localhost:3117" }]); +const page = await ctx.newPage(); +const errs = []; +page.on("pageerror", (e) => errs.push(String(e.message).slice(0, 140))); +await page.goto(`http://localhost:3117/app/${ID}`, { waitUntil: "domcontentloaded" }); +await page.waitForSelector("#report", { timeout: 25000 }); +const q = async (sel) => page.locator(sel).count(); +console.log("QAI report", await q("#report"), "cards", await q("#report .card"), "sql blocks", await q("#report pre.sql"), + "copy buttons", await q("#report button"), "tables", await q("#report table"), "bars", await q("#report .bar"), + "verdict", await q("[data-verdict]"), "metrics", await q(".grid-metrics .card")); +console.log("QAI headline", (await page.locator("#report h2").first().textContent())?.slice(0, 90)); +console.log("QAI console errors", errs.length, errs.slice(0, 2)); +for (const w of [320, 375, 414, 768]) { + await page.setViewportSize({ width: w, height: 800 }); + await page.waitForTimeout(250); + const over = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth); + console.log(`QAI ${w}px overflow ${over}`); +} +await page.setViewportSize({ width: 1280, height: 900 }); +await page.emulateMedia({ media: "print" }); +console.log("QAI print rail hidden", await page.locator(".rail").isHidden(), "toolbar hidden", await page.locator(".toolbar").first().isHidden(), + "body bg", await page.evaluate(() => getComputedStyle(document.body).backgroundColor)); +await b.close(); diff --git a/scripts/render-debug.mjs b/scripts/render-debug.mjs new file mode 100644 index 0000000..8c3ae84 --- /dev/null +++ b/scripts/render-debug.mjs @@ -0,0 +1,13 @@ +import { chromium } from "@playwright/test"; +const ID = process.argv[2]; +const PORT = process.argv[3] ?? "3117"; +const b = await chromium.launch(); +const ctx = await b.newContext(); +await ctx.addCookies([{ name: "qai_s", value: "tokAPI", url: `http://localhost:${PORT}` }]); +const page = await ctx.newPage(); +page.on("console", (m) => { if (m.type() === "error") console.log("QAI console", m.text().slice(0, 400)); }); +page.on("pageerror", (e) => console.log("QAI pageerror", String(e).slice(0, 500))); +await page.goto(`http://localhost:${PORT}/app/${ID}`, { waitUntil: "load" }); +await page.waitForTimeout(6000); +console.log("QAI report count", await page.locator("#report").count()); +await b.close(); diff --git a/tests/db.test.ts b/tests/db.test.ts index f6a0cd2..e2d7242 100644 --- a/tests/db.test.ts +++ b/tests/db.test.ts @@ -37,3 +37,14 @@ test("session resolves only while unexpired", () => { q.dropSession("tok"); assert.equal(q.sessionUserId("tok"), null); }); + +test("rows are plain objects so they can cross the server -> client boundary", () => { + const id = q.createUser("plain@x.y", "h"); + const aid = q.newAnalysis(id, "t", ""); + const row = q.listAnalyses(id)[0]; + assert.equal(Object.getPrototypeOf(row), Object.prototype, "listAnalyses returned a null-prototype row"); + assert.equal(Object.getPrototypeOf(q.getAnalysis(aid, id)!), Object.prototype, "getAnalysis"); + q.saveSettings(id, { analyst_base_url: "b", analyst_key: "k", analyst_model: "m", analyst_extra: "{}", + jev_key: "j", jev_model: "v", onboarded: 1 }); + assert.equal(Object.getPrototypeOf(q.getSettings(id)!), Object.prototype, "getSettings"); +});