From 502d2a304b2137f951244e7ae83ba47ee36b6ab9 Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+maonakamoto@users.noreply.github.com> Date: Thu, 3 Sep 2026 15:00:57 +0200 Subject: [PATCH] feat(feedback): FleetCrown users can finally report bugs in FleetCrown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Asked directly: where is the feedback widget on FleetCrown itself, and how does a user give feedback about the site they are using? Measured on the live app — feedbackWidgetPresent: false, no widget script loaded at all. The only thing in the corner is the Loki assistant button. The widget was scoped to eleven public marketing routes by an allowlist whose comment gave two reasons. Both were checked: "the FAB would collide with the app shell's mobile nav" True when written, and fixed earlier today. The widget now measures its corner and steps aside — rectangle matching for the desktop Loki FAB (56px at bottom:28, which the feedback FAB overlapped exactly), and an interactive hit-test below 480px for the mobile nav, which is inset-x-3 and therefore too wide for the rectangle scan to treat as an obstacle. "in-app feedback already has Loki" Not true. insertSiteFeedback has exactly one caller — the widget's ingest route — and the word "feedback" does not appear in loki-core.ts. Telling the assistant "this button is broken" produces no site_feedback row, nothing in the triage inbox, nothing dispatchable. A signed-in user's only option was to tell the operator out of band. So the rule is inverted: render everywhere EXCEPT a short list, each entry carrying its reason — /terminal (a FAB over live PTY output covers what you are reading), the auth routes (you are not in the product yet), and /docs/feedback-widget (a launcher on the page documenting launchers is an unanswerable "is this yours or the demo's?"). An allowlist meant every new page shipped with no way to report a bug on it, and nobody noticed, because a missing feedback channel is invisible exactly the way the problems it would have caught are. PublicFeedbackWidget is now DogfoodFeedbackWidget: it is no longer public- only, and a name that says otherwise is the kind of stale label this page already had too much of. Pinned by scripts/test/feedback-widget-reach.ts (42 assertions): every app surface reachable, public ones still reachable, exclusions excluded including nested paths, near-misses like /terminals and /settings NOT swallowed by prefix matching, the widget mounted from the root layout so new pages inherit it, and loki-core still unable to file feedback — the premise that justified the old scoping. Proven by mutation: restoring a public-only allowlist turns 20 assertions red, including every app surface. --- scripts/test/feedback-widget-reach.ts | 95 +++++++++++++++++++ src/app/layout.tsx | 12 ++- ...ckWidget.tsx => DogfoodFeedbackWidget.tsx} | 7 +- src/config/feedback-widget.ts | 68 +++++++++---- 4 files changed, 155 insertions(+), 27 deletions(-) create mode 100644 scripts/test/feedback-widget-reach.ts rename src/components/shell/{PublicFeedbackWidget.tsx => DogfoodFeedbackWidget.tsx} (77%) diff --git a/scripts/test/feedback-widget-reach.ts b/scripts/test/feedback-widget-reach.ts new file mode 100644 index 00000000..c1a690b4 --- /dev/null +++ b/scripts/test/feedback-widget-reach.ts @@ -0,0 +1,95 @@ +// A user of FleetCrown must be able to report a bug in FleetCrown. +// +// The widget was scoped to eleven public marketing routes, on the stated +// grounds that "in-app feedback already has Loki". That was not true: +// insertSiteFeedback has exactly one caller — the widget's ingest route — and +// Loki has no feedback capability at all. So every signed-in user, on every +// app page, had no structured way to report anything. The omission was +// invisible, which is the failure mode this whole product exists to fix. +// +// The rule is now an exclusion list, so a NEW page ships with feedback by +// default instead of silently shipping without it. This test pins that +// direction: the app surfaces must be covered, the excluded ones must stay +// excluded, and Loki must not be cited as the in-app answer while it cannot +// file anything. +// Run: npx tsx scripts/test/feedback-widget-reach.ts +import { readFileSync } from "fs"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; +import { + isFeedbackWidgetRoute, + FEEDBACK_WIDGET_EXCLUDED_PREFIXES, +} from "../../src/config/feedback-widget"; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); + +let pass = 0; +let fail = 0; +function ok(cond: boolean, label: string) { + if (cond) { + pass++; + } else { + fail++; + console.error(`✗ ${label}`); + } +} + +// The surfaces a signed-in operator actually works on. Each of these is a +// place someone can hit a bug, so each must be able to report one. +const APP_SURFACES = [ + "/today", + "/control", + "/projects", + "/projects/5c0ea00f-d098-49a0-aae5-c83b8f4be79c", + "/feedback", + "/activity", + "/goals", + "/people", + "/crew", + "/money", + "/habits", + "/prompts", + "/system", + "/settings", + "/loki", + "/approvals", +]; +for (const path of APP_SURFACES) { + ok(isFeedbackWidgetRoute(path), `widget reaches ${path}`); +} + +// Public pages keep it too — that was never in question, but a rewrite of the +// matcher could easily drop them. +for (const path of ["/", "/pricing", "/thoughts", "/thoughts/some-essay"]) { + ok(isFeedbackWidgetRoute(path), `widget still reaches public ${path}`); +} + +// The exclusions must actually exclude, including nested paths. +for (const p of FEEDBACK_WIDGET_EXCLUDED_PREFIXES) { + ok(!isFeedbackWidgetRoute(p), `excluded: ${p}`); + ok(!isFeedbackWidgetRoute(`${p}/nested`), `excluded: ${p}/nested`); +} + +// A near-miss must NOT be excluded — prefix matching that swallows unrelated +// routes is how an allowlist quietly loses pages. +ok(isFeedbackWidgetRoute("/terminals"), "/terminals is not caught by the /terminal exclusion"); +ok(isFeedbackWidgetRoute("/settings"), "/settings is an app page, not an auth page"); + +// The claim that justified the old scoping must not silently return. If Loki +// ever does gain the ability to file feedback, this test should be revisited +// deliberately rather than the assumption creeping back. +const loki = readFileSync(join(ROOT, "src/lib/loki-core.ts"), "utf8"); +ok( + !/insertSiteFeedback/.test(loki), + "loki-core still cannot file feedback — the reason the widget must reach the app", +); + +// The widget mounts from the ROOT layout, so coverage is not per-page opt-in. +const layout = readFileSync(join(ROOT, "src/app/layout.tsx"), "utf8"); +ok( + /DogfoodFeedbackWidget/.test(layout), + "the widget is mounted in the root layout, so new pages get it for free", +); + +console.log(`${pass} passed, ${fail} failed`); +process.exit(fail === 0 ? 0 : 1); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 11c87474..d0a98c7e 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,7 +3,7 @@ import { GeistSans } from "geist/font/sans"; import { GeistMono } from "geist/font/mono"; import { SessionProvider } from "next-auth/react"; import { ThemeProvider } from "@/components/shell/ThemeProvider"; -import { PublicFeedbackWidget } from "@/components/shell/PublicFeedbackWidget"; +import { DogfoodFeedbackWidget } from "@/components/shell/DogfoodFeedbackWidget"; import { APP_DESCRIPTION, APP_NAME, APP_URL } from "@/config/brand"; import { PALETTE } from "@/lib/palette"; import "./globals.css"; @@ -78,10 +78,14 @@ export default function RootLayout({ {children} - {/* Dogfood: FleetCrown's own feedback widget on public pages, active - only where FEEDBACK_WIDGET_TOKEN is provisioned (see config/feedback-widget.ts). */} + {/* Dogfood: FleetCrown's own feedback widget, on every surface except a + short excluded list (config/feedback-widget.ts). Active only where + FEEDBACK_WIDGET_TOKEN is provisioned. It sits in the ROOT layout on + purpose — a signed-in user hitting a bug in the app had no way to + report it, because the widget was scoped to marketing pages and + Loki cannot file feedback. */} {process.env.FEEDBACK_WIDGET_TOKEN && ( - + )} diff --git a/src/components/shell/PublicFeedbackWidget.tsx b/src/components/shell/DogfoodFeedbackWidget.tsx similarity index 77% rename from src/components/shell/PublicFeedbackWidget.tsx rename to src/components/shell/DogfoodFeedbackWidget.tsx index aaf30efe..39372481 100644 --- a/src/components/shell/PublicFeedbackWidget.tsx +++ b/src/components/shell/DogfoodFeedbackWidget.tsx @@ -5,8 +5,9 @@ import { usePathname } from "next/navigation"; import { isFeedbackWidgetRoute } from "@/config/feedback-widget"; /** - * Dogfood embed of FleetCrown's own feedback widget on the public pages - * (docs/architecture/feedback-widget.md, Phase 4). Injects the exact same + * Dogfood embed of FleetCrown's own feedback widget — on EVERY surface, not + * just the public pages (see config/feedback-widget.ts for the short list of + * exclusions and why each one is excluded). Injects the exact same * /widget.js script tag a customer site would use — same code path, same * ingest API — pointed at the project the FEEDBACK_WIDGET_TOKEN env var * belongs to. Rendered by the root layout only when that env var is set. @@ -14,7 +15,7 @@ import { isFeedbackWidgetRoute } from "@/config/feedback-widget"; * Imperative injection (not a JSX