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 && (
-
+
)}