From a9e2fbc612d21567b5901457e36d7a526e5f4457 Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Sun, 4 Oct 2026 14:35:01 +0530 Subject: [PATCH 1/6] fix: use maintained hosted feedback form in SWE --- ...esign-review-hosted-feedback-20261004.json | 211 ++++++++++++++++++ src/components/saasmaker-feedback.test.tsx | 148 ++++++++++-- src/components/saasmaker-feedback.tsx | 166 ++++++++++++-- 3 files changed, 489 insertions(+), 36 deletions(-) create mode 100644 .fleet/design-review-hosted-feedback-20261004.json diff --git a/.fleet/design-review-hosted-feedback-20261004.json b/.fleet/design-review-hosted-feedback-20261004.json new file mode 100644 index 00000000..af3c7465 --- /dev/null +++ b/.fleet/design-review-hosted-feedback-20261004.json @@ -0,0 +1,211 @@ +{ + "$schema": "fleet.design-review.v2", + "version": 2, + "project": "swe-interview-prep", + "target": "Existing hover feedback shared form parity", + "surfaceMode": "operate", + "mode": "preserve", + "register": "product", + "context": { + "product": "PRODUCT.md", + "design": "DESIGN.md" + }, + "direction": { + "productSurfaces": { + "scope": "standalone", + "landing": "", + "app": "https://learn.significanthobbies.com/dashboard", + "reason": "Bounded existing application feedback adapter repair; the public login and curriculum surfaces retain their current identity and routes." + }, + "pairedPreview": { + "landing": "", + "app": "" + }, + "source": "existing", + "references": [], + "probes": [], + "selected": "existing-design", + "approval": "not-required", + "ownerEvidence": { + "path": "", + "quote": "" + }, + "supplied": "", + "before": ".fleet-local/evidence/hosted-feedback-20261004/before.png", + "library": { + "strategy": "upstream-first", + "primary": "existing-project-system", + "sources": [ + "https://sassmaker.com/feedback-launcher.js" + ], + "runtime": "existing", + "customReplacement": { + "used": false, + "authorization": "not-required", + "reason": "" + } + }, + "contract": { + "purpose": "SWE Interview Prep helps software engineers build retained artifact-backed understanding through an evidence-gated study and practice loop.", + "purposeSource": "PRODUCT.md and canonical site-health/docs/project-dossiers/swe-interview-prep.yaml purposeContract", + "canonicalPurpose": "SWE Interview Prep is a personal learning OS that turns software-engineering study into retained, artifact-backed understanding.", + "purposeAlignment": "match", + "driftNote": "", + "audience": "Software engineers studying practical mechanisms and interview skills.", + "job": "Send feedback without interrupting active study or requiring an email address.", + "thesis": "Preserve the quiet black engineering workbench and existing bottom-right feedback affordance; use the maintained shared form.", + "system": "Geist typography; neutral near-black surfaces, near-white text, sparse sky focus accents, 44px trigger, existing floating bottom-right placement.", + "signature": "Feedback remains outside focused learning and shared routes so the evidence-gated practice workbench remains uninterrupted.", + "risk": "Lazy hosted first-party module can fail or drift; show retry state and verify actual deployed consent and focus behavior.", + "qualityBar": "Optional email, explicit unchecked consent and privacy link, keyboard entry/trap/Escape/return, one widget root, no duplicate submissions or initial bundle load." + } + }, + "evidence": { + "platform": "web", + "productContinuity": { + "status": "not-applicable", + "reason": "Bounded existing application feedback adapter repair; the public login and curriculum surfaces retain their current identity and routes.", + "reviewer": "", + "report": "artifacts/design/product-continuity.md", + "surfaces": { + "landing": "artifacts/design/landing.png", + "app": "artifacts/design/app.png" + }, + "checks": { + "designSystem": { + "status": "pending", + "observation": "" + }, + "identity": { + "status": "pending", + "observation": "" + }, + "vocabulary": { + "status": "pending", + "observation": "" + }, + "productTruth": { + "status": "pending", + "observation": "" + }, + "handoff": { + "status": "pending", + "observation": "" + } + } + }, + "slopScale": { + "platform": "web", + "posture": "advisory", + "reason": "", + "checkpoints": [] + }, + "visualReview": { + "status": "pending", + "reviewer": "", + "direction": "", + "report": "artifacts/design/review.md", + "checks": { + "hierarchy": { + "status": "pending", + "observation": "" + }, + "typography": { + "status": "pending", + "observation": "" + }, + "composition": { + "status": "pending", + "observation": "" + }, + "identity": { + "status": "pending", + "observation": "" + }, + "interaction": { + "status": "pending", + "observation": "" + }, + "responsive": { + "status": "pending", + "observation": "" + } + } + }, + "screenshots": [ + { + "width": 390, + "path": "artifacts/design/after-390.png" + }, + { + "width": 768, + "path": "artifacts/design/after-768.png" + }, + { + "width": 1440, + "path": "artifacts/design/after-1440.png" + } + ], + "projectCheck": { + "command": "replace-with-project-check", + "status": "pending" + }, + "critique": { + "score": 0, + "maximum": 40, + "dimensions": { + "hierarchy": 0, + "typography": 0, + "composition": 0, + "identity": 0, + "interaction": 0, + "responsive": 0 + } + }, + "audit": { + "score": 0, + "maximum": 20, + "dimensions": { + "purpose": 0, + "accessibility": 0, + "behavior": 0, + "responsive": 0, + "performance": 0 + } + }, + "unresolved": { + "p0": 0, + "p1": 0 + }, + "detector": { + "posture": "advisory", + "findings": [] + }, + "comprehension": { + "status": "not-applicable", + "reviewer": "", + "answers": { + "product": "", + "audience": "", + "value": "", + "mechanism": "", + "proof": "", + "nextAction": "" + }, + "purposeScore": { + "product": 0, + "audience": 0, + "value": 0, + "mechanism": 0, + "proof": 0, + "nextAction": 0, + "total": 0 + }, + "mismatches": [] + } + }, + "ownerFeedback": { + "decision": "not-required", + "note": "Preserve work follows the existing direction; this is not owner acceptance of the result." + } +} diff --git a/src/components/saasmaker-feedback.test.tsx b/src/components/saasmaker-feedback.test.tsx index d0489bb4..14974168 100644 --- a/src/components/saasmaker-feedback.test.tsx +++ b/src/components/saasmaker-feedback.test.tsx @@ -8,15 +8,10 @@ import { fetchFeedbackProjectKey, SaaSMakerFeedback } from './saasmaker-feedback const { TEST_KEY } = vi.hoisted(() => ({ TEST_KEY: 'pk_testfixture123' })); -vi.mock('@saas-maker/feedback', async () => { - const React = await import('react'); - return { - FeedbackWidget: ({ projectId }: { projectId: string }) => - React.createElement('div', { - 'data-testid': 'feedback-widget', - 'data-project-bound': String(projectId === TEST_KEY), - }), - }; +const launcher = () => ({ + mountSharedFooterFeedback: vi.fn<(node: HTMLElement, options: { apiKey: string }) => void>(), + openSharedFooterFeedback: vi.fn<(node: HTMLElement, options: { apiKey: string }) => void>(), + unmountSharedFooterFeedback: vi.fn<(node: HTMLElement) => void>(), }); describe('SaaS Maker feedback binding', () => { @@ -28,41 +23,156 @@ describe('SaaS Maker feedback binding', () => { document.body.append(container); root = createRoot(container); vi.stubGlobal('fetch', vi.fn()); + delete (window as Window & { SaasMakerFeedback?: unknown }).SaasMakerFeedback; }); afterEach(async () => { await act(async () => root.unmount()); container.remove(); vi.unstubAllGlobals(); + vi.useRealTimers(); + delete (window as Window & { SaasMakerFeedback?: unknown }).SaasMakerFeedback; }); - it('mounts the existing widget only with the catalog-bound publishable key', async () => { + it('loads on intent, mounts with the catalog key, and reopens the same root', async () => { vi.mocked(fetch).mockResolvedValue({ ok: true, json: async () => ({ api_key: TEST_KEY }), } as Response); await act(async () => root.render()); - for ( - let attempt = 0; - attempt < 5 && !container.querySelector('[data-testid="feedback-widget"]'); - attempt += 1 - ) { - await act(async () => new Promise((resolve) => setTimeout(resolve, 0))); - } + expect( + document.querySelector('script[src="https://sassmaker.com/feedback-launcher.js"]') + ).toBeNull(); + const button = container.querySelector('button')!; + await act(async () => button.click()); + expect(button.disabled).toBe(false); + expect(button.getAttribute('aria-busy')).toBe('true'); + expect(document.activeElement).toBe(button); + await act(async () => button.click()); + expect( + document.querySelectorAll('script[src="https://sassmaker.com/feedback-launcher.js"]') + ).toHaveLength(1); + const api = launcher(); + (window as Window & { SaasMakerFeedback?: unknown }).SaasMakerFeedback = api; + const script = document.querySelector( + 'script[src="https://sassmaker.com/feedback-launcher.js"]' + )!; + await act(async () => script.dispatchEvent(new Event('load'))); expect(fetch).toHaveBeenCalledWith( 'https://api.sassmaker.com/v1/capture-config/swe-interview-prep', expect.objectContaining({ headers: { accept: 'application/json' } }) ); - expect(container.querySelector('[data-project-bound="true"]')).not.toBeNull(); + expect(api.mountSharedFooterFeedback).toHaveBeenCalledWith( + expect.any(HTMLDivElement), + expect.objectContaining({ apiKey: TEST_KEY }) + ); + const mountNode = api.mountSharedFooterFeedback.mock.calls[0][0]; + expect(mountNode.children).toHaveLength(0); + await act(async () => button.click()); + expect(api.mountSharedFooterFeedback).toHaveBeenCalledTimes(1); + expect(api.openSharedFooterFeedback).toHaveBeenCalledWith( + mountNode, + expect.objectContaining({ apiKey: TEST_KEY }) + ); + await act(async () => root.render(null)); + expect(api.unmountSharedFooterFeedback).toHaveBeenCalledWith(mountNode); + expect(mountNode.isConnected).toBe(false); + }); + + it('retries a failed module and rejects a loaded module without its API', async () => { + vi.mocked(fetch).mockResolvedValue({ + ok: true, + json: async () => ({ api_key: TEST_KEY }), + } as Response); + await act(async () => root.render()); + const button = container.querySelector('button')!; + await act(async () => button.click()); + const first = document.querySelector( + 'script[src="https://sassmaker.com/feedback-launcher.js"]' + )!; + await act(async () => first.dispatchEvent(new Event('error'))); + expect(container.textContent).toContain('Retry feedback'); + expect(first.isConnected).toBe(false); + await act(async () => button.click()); + const second = document.querySelector( + 'script[src="https://sassmaker.com/feedback-launcher.js"]' + )!; + expect(second).not.toBe(first); + await act(async () => second.dispatchEvent(new Event('load'))); + expect(container.textContent).toContain('Retry feedback'); + await act(async () => button.click()); + const api = launcher(); + (window as Window & { SaasMakerFeedback?: unknown }).SaasMakerFeedback = api; + await act(async () => + document + .querySelector('script[src="https://sassmaker.com/feedback-launcher.js"]')! + .dispatchEvent(new Event('load')) + ); + expect(api.mountSharedFooterFeedback).toHaveBeenCalledTimes(1); + }); + + it('ignores a late script load after unmount and uses a fresh node after remount', async () => { + vi.mocked(fetch).mockResolvedValue({ + ok: true, + json: async () => ({ api_key: TEST_KEY }), + } as Response); + await act(async () => root.render()); + await act(async () => container.querySelector('button')!.click()); + const script = document.querySelector( + 'script[src="https://sassmaker.com/feedback-launcher.js"]' + )!; + await act(async () => root.render(null)); + const api = launcher(); + (window as Window & { SaasMakerFeedback?: unknown }).SaasMakerFeedback = api; + await act(async () => script.dispatchEvent(new Event('load'))); + expect(api.mountSharedFooterFeedback).not.toHaveBeenCalled(); + await act(async () => root.render()); + await act(async () => container.querySelector('button')!.click()); + const first = api.mountSharedFooterFeedback.mock.calls[0][0]; + first.dataset.feedbackMounted = 'true'; + await act(async () => root.render(null)); + await act(async () => root.render()); + await act(async () => container.querySelector('button')!.click()); + const second = api.mountSharedFooterFeedback.mock.calls[1][0]; + expect(second).not.toBe(first); + expect(second.dataset.feedbackMounted).toBeUndefined(); + expect(api.unmountSharedFooterFeedback).toHaveBeenCalledTimes(1); + }); + + it('times out a stalled module, ignores its late callback, and permits retry', async () => { + vi.useFakeTimers(); + vi.mocked(fetch).mockResolvedValue({ + ok: true, + json: async () => ({ api_key: TEST_KEY }), + } as Response); + await act(async () => root.render()); + const button = container.querySelector('button')!; + await act(async () => button.click()); + const script = document.querySelector( + 'script[src="https://sassmaker.com/feedback-launcher.js"]' + )!; + const lateLoad = script.onload!; + await act(async () => vi.advanceTimersByTime(8_000)); + expect(container.textContent).toContain('Retry feedback'); + expect(script.isConnected).toBe(false); + const api = launcher(); + (window as Window & { SaasMakerFeedback?: unknown }).SaasMakerFeedback = api; + await act(async () => lateLoad.call(script, new Event('load'))); + expect(api.mountSharedFooterFeedback).not.toHaveBeenCalled(); + await act(async () => button.click()); + expect(api.mountSharedFooterFeedback).toHaveBeenCalledTimes(1); }); it.each([ ['invalid key shape', { ok: true, json: async () => ({ api_key: 'wrong-project' }) }], [ 'invented publishable suffix', - { ok: true, json: async () => ({ api_key: 'pk_testfixture123_publishable' }) }, + { + ok: true, + json: async () => ({ api_key: 'pk_testfixture123_publishable' }), + }, ], ['unavailable binding', { ok: false, json: async () => ({}) }], ])('fails closed for %s', async (_case, response) => { diff --git a/src/components/saasmaker-feedback.tsx b/src/components/saasmaker-feedback.tsx index 92f555c6..9859ad7f 100644 --- a/src/components/saasmaker-feedback.tsx +++ b/src/components/saasmaker-feedback.tsx @@ -1,21 +1,44 @@ -import '@saas-maker/feedback/dist/index.css'; - -import { lazy, Suspense, useEffect, useState } from 'react'; +import { MessageSquare } from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; const API_BASE = 'https://api.sassmaker.com'; const CATALOG_ID = 'swe-interview-prep'; const PUBLISHABLE_KEY_PATTERN = /^pk_[a-z0-9]+$/; const CONFIG_TIMEOUT_MS = 8_000; -const FeedbackWidget = lazy(async () => { - const mod = await import('@saas-maker/feedback'); - return { default: mod.FeedbackWidget }; -}); +const LAUNCHER_URL = 'https://sassmaker.com/feedback-launcher.js'; +type FeedbackOptions = { apiKey: string; pageUrl: string; pageTitle: string }; +type FeedbackLauncher = { + mountSharedFooterFeedback: (node: HTMLElement, options: FeedbackOptions) => void; + openSharedFooterFeedback: (node: HTMLElement, options: FeedbackOptions) => void; + unmountSharedFooterFeedback: (node: HTMLElement) => void; +}; + +function getLauncher(): FeedbackLauncher | null { + const candidate = (window as Window & { SaasMakerFeedback?: FeedbackLauncher }).SaasMakerFeedback; + return candidate && + typeof candidate.mountSharedFooterFeedback === 'function' && + typeof candidate.openSharedFooterFeedback === 'function' && + typeof candidate.unmountSharedFooterFeedback === 'function' + ? candidate + : null; +} export function SaaSMakerFeedback() { const [projectKey, setProjectKey] = useState(null); + const [status, setStatus] = useState<'idle' | 'loading' | 'error'>('idle'); + const host = useRef(null); + const trigger = useRef(null); + const mounted = useRef<{ + node: HTMLDivElement; + api: FeedbackLauncher; + } | null>(null); + const pending = useRef(null); + const loadTimeout = useRef(null); + const isMounted = useRef(false); useEffect(() => { + isMounted.current = true; const controller = new AbortController(); let active = true; const timeout = window.setTimeout(() => controller.abort(), CONFIG_TIMEOUT_MS); @@ -27,24 +50,133 @@ export function SaaSMakerFeedback() { .finally(() => window.clearTimeout(timeout)); return () => { + isMounted.current = false; active = false; window.clearTimeout(timeout); controller.abort(); + if (loadTimeout.current !== null) window.clearTimeout(loadTimeout.current); + if (pending.current) { + pending.current.onload = null; + pending.current.onerror = null; + pending.current.remove(); + pending.current = null; + } + const widget = mounted.current; + mounted.current = null; + if (widget) { + try { + widget.api.unmountSharedFooterFeedback(widget.node); + } finally { + widget.node.remove(); + } + } }; }, []); + function open(api: FeedbackLauncher) { + if (!isMounted.current || !projectKey || !host.current) return; + // The shared dialog remembers the active element for focus restoration. + trigger.current?.focus(); + const options = { + apiKey: projectKey, + pageUrl: window.location.href, + pageTitle: document.title, + }; + try { + if (mounted.current) { + mounted.current.api.openSharedFooterFeedback(mounted.current.node, options); + } else { + // A fresh node avoids the launcher's retained mounted dataset marker. + const node = document.createElement('div'); + host.current.append(node); + mounted.current = { node, api }; + api.mountSharedFooterFeedback(node, options); + } + setStatus('idle'); + } catch { + const widget = mounted.current; + mounted.current = null; + if (widget) { + try { + widget.api.unmountSharedFooterFeedback(widget.node); + } finally { + widget.node.remove(); + } + } + setStatus('error'); + } + } + + function handleOpen() { + trigger.current?.focus(); + if (pending.current) return; + const api = getLauncher(); + if (api) { + open(api); + return; + } + setStatus('loading'); + const script = document.createElement('script'); + script.src = LAUNCHER_URL; + script.async = true; + pending.current = script; + const finish = () => { + if (loadTimeout.current !== null) window.clearTimeout(loadTimeout.current); + loadTimeout.current = null; + script.onload = null; + script.onerror = null; + script.remove(); + pending.current = null; + }; + script.onload = () => { + if (pending.current !== script) return; + finish(); + if (!isMounted.current) return; + const loaded = getLauncher(); + if (loaded) open(loaded); + else setStatus('error'); + }; + script.onerror = () => { + if (pending.current !== script) return; + finish(); + if (isMounted.current) setStatus('error'); + }; + loadTimeout.current = window.setTimeout(() => { + finish(); + if (isMounted.current) setStatus('error'); + }, CONFIG_TIMEOUT_MS); + document.head.append(script); + } + if (!projectKey) return null; return ( - - - + <> +
+
+ {status === 'error' && ( +

+ Feedback couldn’t load. Please try again. +

+ )} + +
+ ); } From df69e8efe902ff742ff26e809e475ab4f3de5604 Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Sun, 4 Oct 2026 14:38:12 +0530 Subject: [PATCH 2/6] fix: omit query and fragment from feedback context --- src/components/saasmaker-feedback.test.tsx | 11 ++++++++++- src/components/saasmaker-feedback.tsx | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/saasmaker-feedback.test.tsx b/src/components/saasmaker-feedback.test.tsx index 14974168..f6e1a1a3 100644 --- a/src/components/saasmaker-feedback.test.tsx +++ b/src/components/saasmaker-feedback.test.tsx @@ -30,11 +30,17 @@ describe('SaaS Maker feedback binding', () => { await act(async () => root.unmount()); container.remove(); vi.unstubAllGlobals(); + vi.restoreAllMocks(); vi.useRealTimers(); delete (window as Window & { SaasMakerFeedback?: unknown }).SaasMakerFeedback; }); it('loads on intent, mounts with the catalog key, and reopens the same root', async () => { + vi.spyOn(window, 'location', 'get').mockReturnValue({ + origin: 'https://learn.significanthobbies.com', + pathname: '/practice', + href: 'https://learn.significanthobbies.com/practice?invite=testfixture#private', + } as Location); vi.mocked(fetch).mockResolvedValue({ ok: true, json: async () => ({ api_key: TEST_KEY }), @@ -66,7 +72,10 @@ describe('SaaS Maker feedback binding', () => { ); expect(api.mountSharedFooterFeedback).toHaveBeenCalledWith( expect.any(HTMLDivElement), - expect.objectContaining({ apiKey: TEST_KEY }) + expect.objectContaining({ + apiKey: TEST_KEY, + pageUrl: 'https://learn.significanthobbies.com/practice', + }) ); const mountNode = api.mountSharedFooterFeedback.mock.calls[0][0]; expect(mountNode.children).toHaveLength(0); diff --git a/src/components/saasmaker-feedback.tsx b/src/components/saasmaker-feedback.tsx index 9859ad7f..8a4f9164 100644 --- a/src/components/saasmaker-feedback.tsx +++ b/src/components/saasmaker-feedback.tsx @@ -79,7 +79,7 @@ export function SaaSMakerFeedback() { trigger.current?.focus(); const options = { apiKey: projectKey, - pageUrl: window.location.href, + pageUrl: window.location.origin + window.location.pathname, pageTitle: document.title, }; try { From 9e6fd08e52642bc66f191086172c489f96e9cab6 Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Sun, 4 Oct 2026 14:40:54 +0530 Subject: [PATCH 3/6] test: control hosted script events in Happy DOM --- src/components/saasmaker-feedback.test.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/saasmaker-feedback.test.tsx b/src/components/saasmaker-feedback.test.tsx index f6e1a1a3..bf8b3cc8 100644 --- a/src/components/saasmaker-feedback.test.tsx +++ b/src/components/saasmaker-feedback.test.tsx @@ -23,6 +23,15 @@ describe('SaaS Maker feedback binding', () => { document.body.append(container); root = createRoot(container); vi.stubGlobal('fetch', vi.fn()); + const append = document.head.append.bind(document.head); + vi.spyOn(document.head, 'append').mockImplementation((...nodes) => { + // Happy DOM emits an immediate error for disabled external scripts. + // Keep the DOM node inert so each test controls load/error timing. + for (const node of nodes) { + if (node instanceof HTMLScriptElement) node.type = 'application/x-feedback-test'; + } + append(...nodes); + }); delete (window as Window & { SaasMakerFeedback?: unknown }).SaasMakerFeedback; }); From 69e0f31582e4bfb8116d4f55c89573586b904847 Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Sun, 4 Oct 2026 14:51:26 +0530 Subject: [PATCH 4/6] chore: remove legacy feedback runtime dependency --- package.json | 1 - pnpm-lock.yaml | 14 -------------- 2 files changed, 15 deletions(-) diff --git a/package.json b/package.json index 06a89cf9..6567517d 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,6 @@ "@excalidraw/excalidraw": "0.18.1", "@monaco-editor/react": "4.7.0", "@radix-ui/react-tooltip": "^1.2.16", - "@saas-maker/feedback": "^0.2.1", "ai": "6.0.168", "clsx": "^2.1.1", "fflate": "^0.8.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b42f3f68..4a4521b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,9 +48,6 @@ importers: '@saas-maker/app-health': specifier: https://github.com/sass-maker/app-health/releases/download/node-v0.4.0/saas-maker-app-health-0.4.0.tgz version: https://github.com/sass-maker/app-health/releases/download/node-v0.4.0/saas-maker-app-health-0.4.0.tgz - '@saas-maker/feedback': - specifier: ^0.2.1 - version: 0.2.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) ai: specifier: 6.0.168 version: 6.0.168(zod@4.4.3) @@ -2211,12 +2208,6 @@ packages: hono: optional: true - '@saas-maker/feedback@0.2.1': - resolution: {integrity: sha512-ajtrH7t/hhzxrNzX2BU0i5nfGpXNu7w4EQCFBajsuFn255O29trRCu3VNL1NoQhWwypjEEdeaK4DqoEY4zd7rw==} - peerDependencies: - react: '>=18' - react-dom: '>=18' - '@sindresorhus/is@7.2.0': resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} engines: {node: '>=18'} @@ -6309,11 +6300,6 @@ snapshots: '@saas-maker/app-health@https://github.com/sass-maker/app-health/releases/download/node-v0.4.0/saas-maker-app-health-0.4.0.tgz': {} - '@saas-maker/feedback@0.2.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': - dependencies: - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) - '@sindresorhus/is@7.2.0': {} '@size-limit/file@11.2.0(size-limit@11.2.0)': From e91fc369664c270dca8221bd2e69b0bc37d1d473 Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Sun, 4 Oct 2026 15:10:58 +0530 Subject: [PATCH 5/6] ci: retain short-lived browser qualification build --- .github/workflows/ci.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca3c7d83..cbec99cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,8 @@ on: jobs: ci: runs-on: ubuntu-latest + permissions: + contents: read steps: - name: Checkout uses: actions/checkout@v6 @@ -39,3 +41,15 @@ jobs: run: pnpm quality env: VITE_GOOGLE_CLIENT_ID: ci-placeholder.apps.googleusercontent.com + + - name: Retain browser qualification build + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@v7 + with: + name: browser-build-${{ github.event.pull_request.head.sha }} + path: | + dist/** + !dist/wasm/** + if-no-files-found: error + retention-days: 1 + compression-level: 6 From 09a8198b9eecde8b12d6466f6200e58df9c8a06e Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Sun, 4 Oct 2026 15:20:05 +0530 Subject: [PATCH 6/6] docs: record complete app feedback qualification --- ...esign-review-hosted-feedback-20261004.json | 85 +++++++++++-------- 1 file changed, 50 insertions(+), 35 deletions(-) diff --git a/.fleet/design-review-hosted-feedback-20261004.json b/.fleet/design-review-hosted-feedback-20261004.json index af3c7465..b53f2722 100644 --- a/.fleet/design-review-hosted-feedback-20261004.json +++ b/.fleet/design-review-hosted-feedback-20261004.json @@ -98,79 +98,94 @@ "platform": "web", "posture": "advisory", "reason": "", - "checkpoints": [] + "checkpoints": [ + { + "stage": "iteration", + "status": "scanned", + "command": "node /Users/sarthak/Desktop/fleet/saas-maker/tooling/scripts/slop-score.mjs http://127.0.0.1:8794/dashboard --json", + "report": ".fleet-local/evidence/hosted-feedback-20261004/slop-full-app-iteration.json", + "findingsReview": "Geist, permanent dark workbench, and four-item path group are intentional existing DESIGN.md choices. Scan covers initial dashboard, not open modal; modal craft reviewed from actual captures and keyboard evidence." + }, + { + "stage": "final", + "status": "scanned", + "command": "node /Users/sarthak/Desktop/fleet/saas-maker/tooling/scripts/slop-score.mjs http://127.0.0.1:8794/dashboard --json", + "report": ".fleet-local/evidence/hosted-feedback-20261004/slop-full-app-final.json", + "findingsReview": "Geist, permanent dark workbench, and four-item path group are intentional existing DESIGN.md choices. Scan covers initial dashboard, not open modal; modal craft reviewed from actual captures and keyboard evidence." + } + ] }, "visualReview": { - "status": "pending", - "reviewer": "", - "direction": "", - "report": "artifacts/design/review.md", + "status": "pass", + "reviewer": "Root agent direct rendered review", + "direction": "existing-design", + "report": ".fleet-local/evidence/hosted-feedback-20261004/visual-review.md", "checks": { "hierarchy": { - "status": "pending", - "observation": "" + "status": "pass", + "observation": "Direct line, type, required fields, optional fields, consent and Submit retain clear sequence in actual form captures." }, "typography": { - "status": "pending", - "observation": "" + "status": "pass", + "observation": "Geist workbench unchanged; dialog title, labels and focus are legible at all three widths; subdued consent noted in review." }, "composition": { - "status": "pending", - "observation": "" + "status": "pass", + "observation": "Actual guest dashboard remains behind a right-side desktop dialog; compact form has no horizontal overflow." }, "identity": { - "status": "pending", - "observation": "" + "status": "pass", + "observation": "Quiet black learning workbench preserved; shared navy Direct line form intentionally distinct; focused and shared routes remain uninterrupted." }, "interaction": { - "status": "pending", - "observation": "" + "status": "pass", + "observation": "Title gets focus; Tab wraps Submit to Close; Escape and Close restore Feedback; one dialog after reopen; focused study and public roadmap have zero feedback triggers." }, "responsive": { - "status": "pending", - "observation": "" + "status": "pass", + "observation": "Complete CI-built app captured at 390, 768 and 1440; required controls and consent visible, 44px trigger, no horizontal overflow." } } }, "screenshots": [ { "width": 390, - "path": "artifacts/design/after-390.png" + "path": ".fleet-local/evidence/hosted-feedback-20261004/full-app-after-390.png" }, { "width": 768, - "path": "artifacts/design/after-768.png" + "path": ".fleet-local/evidence/hosted-feedback-20261004/full-app-after-768.png" }, { "width": 1440, - "path": "artifacts/design/after-1440.png" + "path": ".fleet-local/evidence/hosted-feedback-20261004/full-app-after-1440.png" } ], "projectCheck": { - "command": "replace-with-project-check", - "status": "pending" + "command": "pnpm quality (GitHub Actions 37192953403 at e91fc369664c270dca8221bd2e69b0bc37d1d473)", + "status": "pass" }, "critique": { - "score": 0, + "score": 35, "maximum": 40, "dimensions": { - "hierarchy": 0, - "typography": 0, - "composition": 0, - "identity": 0, - "interaction": 0, - "responsive": 0 + "hierarchy": 7, + "typography": 7, + "composition": 7, + "identity": 6, + "interaction": 4, + "responsive": 4 } }, "audit": { - "score": 0, + "score": 17, "maximum": 20, "dimensions": { - "purpose": 0, - "accessibility": 0, - "behavior": 0, - "responsive": 0, - "performance": 0 + "purpose": 4, + "accessibility": 5, + "behavior": 3, + "responsive": 3, + "performance": 2 } }, "unresolved": {