@mhosaic/feedback-cli 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -0
- package/dist/bin.js +14 -4
- package/dist/bin.js.map +1 -1
- package/dist/install-skill-CIZ3ZQI7.js +88 -0
- package/dist/install-skill-CIZ3ZQI7.js.map +1 -0
- package/dist/verify-LCXL3WOX.js +188 -0
- package/dist/verify-LCXL3WOX.js.map +1 -0
- package/package.json +4 -2
- package/skills/integrate-feedback/SKILL.md +142 -0
- package/skills/integrate-feedback/references/consumer-install-next.md +133 -0
- package/skills/integrate-feedback/references/consumer-install-plain.md +92 -0
- package/skills/integrate-feedback/references/consumer-install-remix.md +93 -0
- package/skills/integrate-feedback/references/consumer-install-svelte.md +87 -0
- package/skills/integrate-feedback/references/consumer-install-vite.md +100 -0
- package/skills/integrate-feedback/references/consumer-install-vue.md +92 -0
- package/skills/integrate-feedback/references/consumer-install.md +179 -0
- package/skills/integrate-feedback/references/identify-snippets.md +218 -0
- package/skills/integrate-feedback/references/operator-provision.md +233 -0
- package/skills/integrate-feedback/references/verify-install.md +151 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Consumer mode — installing the widget
|
|
2
|
+
|
|
3
|
+
You are guiding a teammate through installing the @mhosaic/feedback widget into their host app, given a handoff payload from the operator.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Step 1 — Parse the handoff payload
|
|
8
|
+
|
|
9
|
+
Ask the user to paste the handoff payload as plain Markdown. Extract:
|
|
10
|
+
|
|
11
|
+
- `Endpoint:` the backend URL
|
|
12
|
+
- `Public widget key:` the `pk_proj_…` token
|
|
13
|
+
- `Allowed origin(s):` the comma-separated list (used to verify CORS later)
|
|
14
|
+
- `Project slug:` (optional; used only for human-readable error messages)
|
|
15
|
+
|
|
16
|
+
If the user can't find the payload, walk them through asking the operator for it. The format is documented in `SKILL.md` under "handoff payload format".
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Step 2 — Verify we're in a JS/TS project
|
|
21
|
+
|
|
22
|
+
`Read` the user's `package.json`. If the file doesn't exist, stop and ask:
|
|
23
|
+
|
|
24
|
+
> "I don't see a `package.json` in `<cwd>`. Are you in your host app's project root? cd there and re-run `/integrate-feedback`."
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Step 3 — Detect the framework
|
|
29
|
+
|
|
30
|
+
The CLI does framework detection itself, but you should know the result *before* running it so you can decide which post-install snippet (if any) the user needs to apply.
|
|
31
|
+
|
|
32
|
+
Read `package.json`'s `dependencies` + `devDependencies` and classify:
|
|
33
|
+
|
|
34
|
+
| Found | Framework | Reference doc | CLI auto-wires entry? |
|
|
35
|
+
|---|---|---|---|
|
|
36
|
+
| `vite` + `react` + `@vitejs/plugin-react` | `vite-react` | `consumer-install-vite.md` | ✅ yes |
|
|
37
|
+
| `next` | `nextjs` (App or Pages Router — sniff `app/` vs `pages/`) | `consumer-install-next.md` | ❌ paste snippet |
|
|
38
|
+
| `@remix-run/react` | `remix` | `consumer-install-remix.md` | ❌ paste snippet |
|
|
39
|
+
| `vue` ≥ 3 | `vue` | `consumer-install-vue.md` | ❌ paste snippet |
|
|
40
|
+
| `@sveltejs/kit` | `sveltekit` | `consumer-install-svelte.md` | ❌ paste snippet |
|
|
41
|
+
| none of the above | `plain` / `unknown` | `consumer-install-plain.md` | ❌ CDN snippet |
|
|
42
|
+
|
|
43
|
+
Tell the user which framework you detected. Confirm with `AskUserQuestion` (especially if Next.js, since App vs Pages Router branches differently).
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Step 4 — Run the CLI init (non-interactive)
|
|
48
|
+
|
|
49
|
+
Run from the user's project root:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npx @mhosaic/feedback-cli@latest init \
|
|
53
|
+
--api-key <pk_proj_…> \
|
|
54
|
+
--endpoint <endpoint> \
|
|
55
|
+
--yes
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This will:
|
|
59
|
+
- `npm install @mhosaic/feedback` (using the project's package manager)
|
|
60
|
+
- Write `.env.local` with `VITE_FEEDBACK_API_KEY=…` + `VITE_FEEDBACK_ENDPOINT=…` (Vite-flavored names; rename per framework if Next/Vue/etc. — see framework references)
|
|
61
|
+
- Ensure `.env.local` is in `.gitignore`
|
|
62
|
+
- If `vite-react`: auto-wrap `src/main.tsx` with `<FeedbackProvider>`
|
|
63
|
+
|
|
64
|
+
Surface the CLI's output verbatim. If install fails, stop and read the error message; common causes:
|
|
65
|
+
- No internet
|
|
66
|
+
- Lockfile conflict (offer to delete `pnpm-lock.yaml` / `yarn.lock` / `package-lock.json` and retry)
|
|
67
|
+
- Wrong directory (no `package.json`)
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Step 5 — Framework-specific wiring
|
|
72
|
+
|
|
73
|
+
Open the per-framework reference document and follow it verbatim:
|
|
74
|
+
|
|
75
|
+
- Vite + React: `consumer-install-vite.md` (verify the auto-wrap)
|
|
76
|
+
- Next.js: `consumer-install-next.md`
|
|
77
|
+
- Remix: `consumer-install-remix.md`
|
|
78
|
+
- Vue: `consumer-install-vue.md`
|
|
79
|
+
- SvelteKit: `consumer-install-svelte.md`
|
|
80
|
+
- Plain HTML / CDN: `consumer-install-plain.md`
|
|
81
|
+
|
|
82
|
+
Each reference doc gives:
|
|
83
|
+
1. The env-var prefix (e.g. `NEXT_PUBLIC_FEEDBACK_API_KEY` for Next.js — the CLI's default `VITE_FEEDBACK_*` needs renaming)
|
|
84
|
+
2. The entry-point snippet to paste (with import + provider/init call)
|
|
85
|
+
3. Where to put it (exact file path)
|
|
86
|
+
4. Gotchas (SSR, hydration, client-only)
|
|
87
|
+
|
|
88
|
+
Use the `Edit` tool to make the changes if the user agrees, OR print the snippet and ask the user to apply it manually if they prefer hand control.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Step 6 — Wire identify()
|
|
93
|
+
|
|
94
|
+
Read `references/identify-snippets.md`. Detect the user's auth provider from their dependencies (`@auth0/auth0-react`, `@clerk/clerk-react`, `@supabase/supabase-js`, `firebase`, `next-auth`, `django` session via fetch, custom JWT, anonymous). Paste the matching snippet (with `Edit` tool) into the right place.
|
|
95
|
+
|
|
96
|
+
If you can't detect the auth provider from `package.json`, ask the user explicitly with `AskUserQuestion`.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Step 7 — (Optional but recommended) Wire the auto-capture modules
|
|
101
|
+
|
|
102
|
+
`AskUserQuestion`:
|
|
103
|
+
- question: `Wire the auto-capture modules?`
|
|
104
|
+
- options:
|
|
105
|
+
- `Yes — all three (error-tracking, web-vitals, replay)`, description: `Captures runtime errors, Core Web Vitals, and session replay. ~30 KiB gzip overhead.`
|
|
106
|
+
- `Yes — error-tracking only`, description: `Smallest add. Auto-files synthetic reports for window.onerror + unhandledrejection.`
|
|
107
|
+
- `No — keep it minimal`, description: `Just the manual-submit FAB.`
|
|
108
|
+
|
|
109
|
+
For each `Yes`, modify the consumer's widget init to chain the wrappers. The pattern:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { createFeedback } from '@mhosaic/feedback'
|
|
113
|
+
import { withErrorTracking } from '@mhosaic/feedback/error-tracking'
|
|
114
|
+
import { withReplay } from '@mhosaic/feedback/replay'
|
|
115
|
+
import { withWebVitals } from '@mhosaic/feedback/webvitals'
|
|
116
|
+
|
|
117
|
+
const fb = withErrorTracking(
|
|
118
|
+
withReplay(
|
|
119
|
+
withWebVitals(createFeedback({ apiKey, endpoint }))
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The CLI's auto-wrap uses `<FeedbackProvider>` (a React-specific helper) — for non-Vite-React frameworks the snippet above is the direct equivalent. Match the framework's import style (CommonJS vs ESM, top-level await vs inside an effect).
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Step 8 — Run `verify`
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
npx @mhosaic/feedback-cli@latest verify \
|
|
132
|
+
--origin <consumer-dev-origin> \
|
|
133
|
+
--with-test-report
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
This is a lightweight check; it confirms:
|
|
137
|
+
- `.env.local` has the right shape
|
|
138
|
+
- The endpoint resolves and responds
|
|
139
|
+
- CORS allows the consumer's origin
|
|
140
|
+
- The public key is accepted (POSTs a `[mhosaic-feedback verify]` synthetic report)
|
|
141
|
+
|
|
142
|
+
If anything is red, the verify command prints a hint. Common hints:
|
|
143
|
+
- `add this origin to the project's allowed_origins (admin SPA: Edit project)` → operator needs to add the dev/prod origin.
|
|
144
|
+
- `key invalid or revoked` → operator needs to mint a new key.
|
|
145
|
+
|
|
146
|
+
Stop on red and surface the hint. Don't proceed to step 9 until verify is green.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Step 9 — Live smoke test in Chrome
|
|
151
|
+
|
|
152
|
+
Follow `references/verify-install.md` for the full end-to-end smoke test:
|
|
153
|
+
|
|
154
|
+
1. Start the consumer's dev server (`pnpm dev` / `npm run dev`)
|
|
155
|
+
2. Drive Chrome to the dev URL
|
|
156
|
+
3. Screenshot — confirm the FAB renders (bottom-right corner)
|
|
157
|
+
4. Click the FAB, fill the form with `[INTEGRATE-FEEDBACK SMOKE TEST]`, submit
|
|
158
|
+
5. Drive Chrome to `https://software-factory-3tbbu.ondigitalocean.app/reports` and confirm the report landed
|
|
159
|
+
|
|
160
|
+
Both verify (step 8) and smoke test (step 9) write to the report stream — the operator can clean them up after a successful integration (admin SPA → /reports → filter by description → bulk delete).
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Step 10 — Done
|
|
165
|
+
|
|
166
|
+
Summarize for the user:
|
|
167
|
+
|
|
168
|
+
- Widget installed ✓
|
|
169
|
+
- FAB renders ✓
|
|
170
|
+
- Smoke-test report received ✓
|
|
171
|
+
- Optional modules wired: [error-tracking / replay / web-vitals / none]
|
|
172
|
+
- Next: deploy to staging, add the staging URL to `allowed_origins` (operator action — DM them the URL)
|
|
173
|
+
|
|
174
|
+
Encourage them to:
|
|
175
|
+
- Commit the change (sans `.env.local`)
|
|
176
|
+
- Tell the operator the smoke test passed
|
|
177
|
+
- Delete the smoke-test reports from `/reports` in admin once it lands
|
|
178
|
+
|
|
179
|
+
If a follow-up question comes up (e.g. "how do I add another origin?"), point them to the operator's `/integrate-feedback` provisioning mode (operator can re-run it with the same project slug to update origins) OR direct the operator to edit the project in the admin SPA at `/projects/<id>`.
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# identify() recipes per auth provider
|
|
2
|
+
|
|
3
|
+
The widget's FAB is hidden until `fb.identify()` is called with a non-empty `id`. Each snippet below is the minimum-viable wiring for one auth provider. Pick the one that matches the consumer's `package.json`.
|
|
4
|
+
|
|
5
|
+
**General rule:** call `identify()` from a reactive scope that runs *after* the auth SDK resolves the user. Never at module top level. Each snippet below honors that.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Auth0 (`@auth0/auth0-react`)
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
// src/components/FeedbackIdentity.tsx
|
|
13
|
+
'use client' // for Next.js App Router; omit for Vite/Remix
|
|
14
|
+
|
|
15
|
+
import { useEffect } from 'react'
|
|
16
|
+
import { useAuth0 } from '@auth0/auth0-react'
|
|
17
|
+
import { useFeedback } from '@mhosaic/feedback/react'
|
|
18
|
+
|
|
19
|
+
export function FeedbackIdentity() {
|
|
20
|
+
const { user, isAuthenticated, isLoading } = useAuth0()
|
|
21
|
+
const fb = useFeedback()
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (isLoading) return
|
|
24
|
+
if (isAuthenticated && user) {
|
|
25
|
+
fb.identify({ id: user.sub ?? '', email: user.email, name: user.name })
|
|
26
|
+
} else {
|
|
27
|
+
fb.identify({ id: '', email: '', name: '' })
|
|
28
|
+
}
|
|
29
|
+
}, [fb, isAuthenticated, isLoading, user])
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Mount `<FeedbackIdentity />` once inside both `<Auth0Provider>` and `<FeedbackProvider>` (or wherever you instantiated the widget). Hides the FAB on logout, re-identifies on token refresh.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 2. Clerk (`@clerk/clerk-react`)
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { useEffect } from 'react'
|
|
42
|
+
import { useUser } from '@clerk/clerk-react'
|
|
43
|
+
import { useFeedback } from '@mhosaic/feedback/react'
|
|
44
|
+
|
|
45
|
+
export function FeedbackIdentity() {
|
|
46
|
+
const { isLoaded, isSignedIn, user } = useUser()
|
|
47
|
+
const fb = useFeedback()
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!isLoaded) return
|
|
50
|
+
if (isSignedIn && user) {
|
|
51
|
+
fb.identify({
|
|
52
|
+
id: user.id,
|
|
53
|
+
email: user.primaryEmailAddress?.emailAddress,
|
|
54
|
+
name: user.fullName ?? user.username ?? undefined,
|
|
55
|
+
})
|
|
56
|
+
} else fb.identify({ id: '', email: '', name: '' })
|
|
57
|
+
}, [fb, isLoaded, isSignedIn, user])
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Mount under both `<ClerkProvider>` and your widget provider.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 3. Supabase Auth
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// src/lib/feedback.ts (or wherever you instantiated `fb`)
|
|
70
|
+
import { supabase } from './supabaseClient'
|
|
71
|
+
import { fb } from './feedback' // your createFeedback() result
|
|
72
|
+
|
|
73
|
+
supabase.auth.onAuthStateChange((_event, session) => {
|
|
74
|
+
const u = session?.user
|
|
75
|
+
if (u) {
|
|
76
|
+
fb.identify({
|
|
77
|
+
id: u.id,
|
|
78
|
+
email: u.email,
|
|
79
|
+
name: (u.user_metadata?.full_name as string) ?? u.email,
|
|
80
|
+
})
|
|
81
|
+
} else {
|
|
82
|
+
fb.identify({ id: '', email: '', name: '' })
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Place this at app boot, after `createFeedback()`. The callback fires once on subscribe with the current session — no separate `getSession()` needed.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 4. Firebase Auth
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { getAuth, onAuthStateChanged } from 'firebase/auth'
|
|
95
|
+
import { fb } from './feedback'
|
|
96
|
+
|
|
97
|
+
onAuthStateChanged(getAuth(), (u) => {
|
|
98
|
+
if (u) {
|
|
99
|
+
fb.identify({
|
|
100
|
+
id: u.uid,
|
|
101
|
+
email: u.email ?? undefined,
|
|
102
|
+
name: u.displayName ?? undefined,
|
|
103
|
+
})
|
|
104
|
+
} else {
|
|
105
|
+
fb.identify({ id: '', email: '', name: '' })
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Place after `initializeApp()` and `createFeedback()`. The callback fires immediately with the current user (or `null`).
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 5. NextAuth.js / Auth.js (client component)
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
'use client'
|
|
118
|
+
|
|
119
|
+
import { useEffect } from 'react'
|
|
120
|
+
import { useSession } from 'next-auth/react'
|
|
121
|
+
import { useFeedback } from '@mhosaic/feedback/react'
|
|
122
|
+
|
|
123
|
+
export function FeedbackIdentity() {
|
|
124
|
+
const { data: session, status } = useSession()
|
|
125
|
+
const fb = useFeedback()
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (status === 'loading') return
|
|
128
|
+
const u = session?.user
|
|
129
|
+
if (u) {
|
|
130
|
+
fb.identify({
|
|
131
|
+
id: (u as { id?: string }).id ?? u.email ?? '',
|
|
132
|
+
email: u.email ?? undefined,
|
|
133
|
+
name: u.name ?? undefined,
|
|
134
|
+
})
|
|
135
|
+
} else fb.identify({ id: '', email: '', name: '' })
|
|
136
|
+
}, [fb, session, status])
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Mount under `<SessionProvider>`. NextAuth's default `session.user` has no `id` — either add one via the `session` callback in your `[...nextauth]/route.ts`, or fall back to `email` (as above).
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## 6. Django session-based auth
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
import { fb } from './feedback'
|
|
149
|
+
|
|
150
|
+
async function syncFeedbackIdentity() {
|
|
151
|
+
try {
|
|
152
|
+
const res = await fetch('/api/auth/me/', { credentials: 'include' })
|
|
153
|
+
if (!res.ok) { fb.identify({ id: '', email: '', name: '' }); return }
|
|
154
|
+
const u = await res.json()
|
|
155
|
+
fb.identify({ id: String(u.id), email: u.email, name: u.name })
|
|
156
|
+
} catch {
|
|
157
|
+
fb.identify({ id: '', email: '', name: '' })
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
syncFeedbackIdentity()
|
|
161
|
+
// Re-call from your login-success + logout handlers
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
No push channel from Django sessions — you call this at boot, on login, on logout. If you have a SWR/React Query hook for `/api/auth/me/`, mirror the identity call into the same hook's `onSuccess`.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## 7. Custom JWT in `localStorage`
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { fb } from './feedback'
|
|
172
|
+
|
|
173
|
+
function decodeJwt(t: string): { sub?: string; email?: string; name?: string; exp?: number } {
|
|
174
|
+
return JSON.parse(atob(t.split('.')[1]!))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function syncIdentity() {
|
|
178
|
+
const token = localStorage.getItem('jwt')
|
|
179
|
+
if (!token) return fb.identify({ id: '', email: '', name: '' })
|
|
180
|
+
try {
|
|
181
|
+
const { sub, email, name, exp } = decodeJwt(token)
|
|
182
|
+
if (exp && exp * 1000 < Date.now()) return fb.identify({ id: '', email: '', name: '' })
|
|
183
|
+
fb.identify({ id: String(sub ?? ''), email, name })
|
|
184
|
+
} catch { fb.identify({ id: '', email: '', name: '' }) }
|
|
185
|
+
}
|
|
186
|
+
syncIdentity()
|
|
187
|
+
window.addEventListener('storage', (e) => { if (e.key === 'jwt') syncIdentity() })
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The `storage` listener catches logout-in-another-tab. Decoding without verification is fine because the widget identity is not a trust boundary — the backend re-verifies on report submission. For attested identity (`userHash` + `exp` HMAC), compute the envelope server-side and pass it through verbatim.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## 8. Anonymous-only (no auth)
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { fb } from './feedback'
|
|
198
|
+
|
|
199
|
+
// FAB only renders for non-empty `id`. Use a stable browser id so reports
|
|
200
|
+
// from the same anonymous visitor cluster together on the dashboard.
|
|
201
|
+
const id = localStorage.getItem('mhosaic-anon-id')
|
|
202
|
+
?? (() => {
|
|
203
|
+
const fresh = `anon-${crypto.randomUUID()}`
|
|
204
|
+
localStorage.setItem('mhosaic-anon-id', fresh)
|
|
205
|
+
return fresh
|
|
206
|
+
})()
|
|
207
|
+
fb.identify({ id, name: 'Anonymous' })
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Place immediately after `createFeedback()`. Module-top-level is fine here — no async wait needed.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Notes
|
|
215
|
+
|
|
216
|
+
- **Hide the FAB on logout:** every snippet calls `identify({id: '', …})` on the logged-out branch. The widget's visibility gate (see `packages/core/src/core.ts:154`) hides the FAB whenever `id` is empty.
|
|
217
|
+
- **Re-identify on token refresh is free.** Each call swaps the closure-held user reference; there's no remount overhead.
|
|
218
|
+
- **Signed identity (`userHash` + `exp`) is not covered here** because it requires the host backend to compute an HMAC envelope with the project's signing secret. Issue the secret via `python manage.py feedback_issue_signing_secret --project <slug>`, store it in the host's vault, sign on every page render, and pass `userHash` + `exp` through to `identify()` verbatim alongside `id` / `email` / `name`.
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# Operator mode — provisioning a new project
|
|
2
|
+
|
|
3
|
+
You are guiding the platform operator (Victor or a delegate) through creating a new **Company → Project → public widget key** on the Mhosaic backend, then assembling a handoff payload the consumer pastes into their own integration flow.
|
|
4
|
+
|
|
5
|
+
**Strategy:** drive the operator's already-authenticated Chrome session at the software-factory admin SPA. For the API calls themselves, use `mcp__claude-in-chrome__javascript_tool` to invoke `fetch()` *from within* the browser tab — that way every call goes through the SPA's auth cookies, hits the DRF stack with full middleware (CSRF, permissions, audit), and never requires copying tokens into shell `curl` commands.
|
|
6
|
+
|
|
7
|
+
The admin SPA is at https://software-factory-3tbbu.ondigitalocean.app.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Step 1 — Confirm Chrome MCP is available
|
|
12
|
+
|
|
13
|
+
Before anything else, call `mcp__claude-in-chrome__tabs_context_mcp` to discover what tabs exist. If you don't see a tab pointed at `software-factory-3tbbu.ondigitalocean.app`, create one with `mcp__claude-in-chrome__tabs_create_mcp`.
|
|
14
|
+
|
|
15
|
+
If `tabs_context_mcp` errors with "no MCP tab group", call it again with `createIfEmpty: true` — that opens a fresh window for the skill to use without disturbing the operator's existing browsing.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Step 2 — Verify operator is logged in
|
|
20
|
+
|
|
21
|
+
Navigate the chosen tab to `https://software-factory-3tbbu.ondigitalocean.app/`. Take a screenshot. If the page shows `/login`, walk the operator through the SSO flow:
|
|
22
|
+
|
|
23
|
+
> "You're not logged in. Click 'Continuer avec Google' and pick your @mhosaic.com account. When you land on /, tell me 'ready'."
|
|
24
|
+
|
|
25
|
+
Wait for the operator to confirm. Then re-screenshot to verify the dashboard is showing.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Step 3 — Collect the project inputs
|
|
30
|
+
|
|
31
|
+
Use `AskUserQuestion` for **each** of these, one at a time. Always confirm before proceeding.
|
|
32
|
+
|
|
33
|
+
1. **Company:** existing or new?
|
|
34
|
+
- Use `AskUserQuestion` with options `Use existing company` and `Create new company`.
|
|
35
|
+
- If existing: fetch the list with the JS snippet in step 4 and present a multi-choice question.
|
|
36
|
+
- If new: ask for the company name (plain text). Auto-slugify it (lowercase, hyphens, strip accents — see `apps/admin/src/features/companies/NewCompanyForm.tsx:14-22`).
|
|
37
|
+
|
|
38
|
+
2. **Project name** (plain text, e.g. `"NoRag Production"`).
|
|
39
|
+
|
|
40
|
+
3. **Project slug** (plain text, auto-slugified default from name). Must be unique within the company. Lowercase, hyphens, ≤ 80 chars.
|
|
41
|
+
|
|
42
|
+
4. **Allowed origins** (plain text, comma- or space-separated list). Each must be a full `https?://host[:port]` URL. Rules:
|
|
43
|
+
- Wildcards (`*`) are rejected.
|
|
44
|
+
- `http://` is only permitted for localhost / 127.0.0.1 / ::1.
|
|
45
|
+
- Path is forbidden (e.g. `https://app.example.com/widget` — drop the path).
|
|
46
|
+
- Strongly recommend including BOTH the prod URL **and** the dev URL (e.g. `https://app.acme.com, http://localhost:5173`) so the teammate can test locally before deploying.
|
|
47
|
+
|
|
48
|
+
5. **Share reports with widget board?** (`AskUserQuestion`, default No).
|
|
49
|
+
- `Yes — every authenticated end-user sees every report` (good for internal tools)
|
|
50
|
+
- `No — strict submitter-only view` (default, safer for public-facing apps)
|
|
51
|
+
|
|
52
|
+
6. **(Optional) Repo/staging/prod URLs** — surface in admin UI but not load-bearing. Skip unless the operator volunteers.
|
|
53
|
+
|
|
54
|
+
Display the collected values as a summary table and confirm with `AskUserQuestion` "Proceed?" before mutating anything.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Step 4 — Provision via the browser's fetch()
|
|
59
|
+
|
|
60
|
+
You'll execute small JavaScript snippets in the admin SPA tab via `mcp__claude-in-chrome__javascript_tool`. The SPA's session cookies authenticate the requests automatically.
|
|
61
|
+
|
|
62
|
+
### Get a fresh access token
|
|
63
|
+
|
|
64
|
+
The SPA holds its access token in memory (not localStorage — see `apps/admin/src/api/tokenStore.ts`). To get one from outside the SPA's React state, hit the cookie-backed refresh endpoint: the browser auto-sends the HttpOnly refresh cookie, and the response body returns a fresh access token.
|
|
65
|
+
|
|
66
|
+
```javascript
|
|
67
|
+
fetch('/api/auth/token/refresh/', { method: 'POST', credentials: 'include' })
|
|
68
|
+
.then(r => r.json())
|
|
69
|
+
.then(d => JSON.stringify({ access: d.access ?? d.access_token ?? null }))
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
If `access` is null and the response was 401, the operator's session is fully expired — re-do step 2 (re-SSO). Otherwise capture the token and use it for `Authorization: Bearer <token>` on subsequent calls.
|
|
73
|
+
|
|
74
|
+
For the rest of this procedure, treat the captured token as a shell variable `TOKEN`. Each snippet below uses `TOKEN` literally — substitute it before sending to `mcp__claude-in-chrome__javascript_tool`.
|
|
75
|
+
|
|
76
|
+
### Look up or create the company
|
|
77
|
+
|
|
78
|
+
To list companies (response wraps them in `{items: [...]}`):
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
fetch('/api/feedback/v1/companies/', {
|
|
82
|
+
headers: { 'Authorization': 'Bearer TOKEN' }
|
|
83
|
+
}).then(r => r.json()).then(d => JSON.stringify(d.items ?? d))
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Each item has `{id, slug, name}`. Filter client-side by slug if you're looking up an existing company.
|
|
87
|
+
|
|
88
|
+
To create a new company:
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
fetch('/api/feedback/v1/companies/', {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: {
|
|
94
|
+
'Authorization': 'Bearer TOKEN',
|
|
95
|
+
'Content-Type': 'application/json',
|
|
96
|
+
},
|
|
97
|
+
body: JSON.stringify({ name: 'COMPANY_NAME', slug: 'company-slug' })
|
|
98
|
+
}).then(r => r.json()).then(d => JSON.stringify(d))
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Capture the company `id` (UUID) from the response.
|
|
102
|
+
|
|
103
|
+
### Create the project
|
|
104
|
+
|
|
105
|
+
```javascript
|
|
106
|
+
fetch('/api/feedback/v1/projects/', {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: {
|
|
109
|
+
'Authorization': 'Bearer TOKEN',
|
|
110
|
+
'Content-Type': 'application/json',
|
|
111
|
+
},
|
|
112
|
+
body: JSON.stringify({
|
|
113
|
+
company: 'COMPANY_UUID',
|
|
114
|
+
name: 'PROJECT_NAME',
|
|
115
|
+
slug: 'project-slug',
|
|
116
|
+
allowed_origins: ['https://app.example.com', 'http://localhost:5173'],
|
|
117
|
+
share_reports_with_widget: false
|
|
118
|
+
})
|
|
119
|
+
}).then(r => r.json()).then(d => JSON.stringify(d))
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
If you get a 400 with `allowed_origins` validation error: parse the message, walk the operator through fixing the offending URL, and retry. Common reasons:
|
|
123
|
+
- `http://` in prod (use `https://`)
|
|
124
|
+
- trailing slash or path on the URL (strip it)
|
|
125
|
+
- wildcard `*` (not supported by design)
|
|
126
|
+
|
|
127
|
+
If you get a 400 with slug collision: append `-2`, `-3`, … and retry. Confirm the new slug with the operator first.
|
|
128
|
+
|
|
129
|
+
Capture the project `id` from the response.
|
|
130
|
+
|
|
131
|
+
### Mint the public widget key
|
|
132
|
+
|
|
133
|
+
```javascript
|
|
134
|
+
fetch('/api/feedback/v1/project-keys/create/', {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: {
|
|
137
|
+
'Authorization': 'Bearer TOKEN',
|
|
138
|
+
'Content-Type': 'application/json',
|
|
139
|
+
},
|
|
140
|
+
body: JSON.stringify({
|
|
141
|
+
project_id: 'PROJECT_UUID',
|
|
142
|
+
kind: 'public',
|
|
143
|
+
label: 'Production widget key — minted by /integrate-feedback'
|
|
144
|
+
})
|
|
145
|
+
}).then(r => r.json()).then(d => JSON.stringify(d))
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The response includes `key: "pk_proj_…"` in plaintext. **This is the only time the plaintext key is ever returned.** Capture it immediately and treat it as a secret in your subsequent skill state (don't echo it more than necessary).
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Step 5 — Assemble the handoff payload
|
|
153
|
+
|
|
154
|
+
Construct this Markdown block and present it to the operator as the deliverable:
|
|
155
|
+
|
|
156
|
+
````markdown
|
|
157
|
+
## Mhosaic Feedback handoff
|
|
158
|
+
|
|
159
|
+
**Project slug:** `<project-slug>`
|
|
160
|
+
**Project ID:** `<project-uuid>`
|
|
161
|
+
**Endpoint:** `https://software-factory-3tbbu.ondigitalocean.app`
|
|
162
|
+
**Public widget key:** `<pk_proj_…>`
|
|
163
|
+
**Allowed origin(s):** `<origin1>`, `<origin2>`, …
|
|
164
|
+
**Reports visibility:** `<submitter-only | shared with widget board>`
|
|
165
|
+
|
|
166
|
+
### Send to your teammate
|
|
167
|
+
|
|
168
|
+
To install:
|
|
169
|
+
```bash
|
|
170
|
+
npx @mhosaic/feedback-cli@latest init \
|
|
171
|
+
--api-key <pk_proj_…> \
|
|
172
|
+
--endpoint https://software-factory-3tbbu.ondigitalocean.app \
|
|
173
|
+
--yes
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
To verify after install:
|
|
177
|
+
```bash
|
|
178
|
+
npx @mhosaic/feedback-cli@latest verify \
|
|
179
|
+
--origin <consumer dev origin> \
|
|
180
|
+
--with-test-report
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Or guide them with Claude Code:
|
|
184
|
+
```
|
|
185
|
+
/integrate-feedback
|
|
186
|
+
```
|
|
187
|
+
(then paste the block above as their first message)
|
|
188
|
+
````
|
|
189
|
+
|
|
190
|
+
Print this to the chat. Encourage the operator to share it via a secure channel (1Password, Slack DM, signed email) — **not in public channels or git**.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Step 6 — Verify the project really exists
|
|
195
|
+
|
|
196
|
+
Drive Chrome to `https://software-factory-3tbbu.ondigitalocean.app/companies/<company-slug>` and screenshot. The operator should see the project listed. Then drive to `/settings/widget-keys` and confirm the key shows up there (masked — the plaintext is gone after this point).
|
|
197
|
+
|
|
198
|
+
If the project doesn't appear: something silently failed (browser cache, deploy mid-rollout, race condition). Open browser DevTools network tab via `mcp__claude-in-chrome__read_network_requests` filtered to `/api/feedback/v1/projects/` and inspect the actual POST response. Surface the diagnostic to the operator and stop.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Step 7 — Offer to continue into consumer mode
|
|
203
|
+
|
|
204
|
+
`AskUserQuestion`:
|
|
205
|
+
- question: `Want to install the widget into a consumer app right now?`
|
|
206
|
+
- options:
|
|
207
|
+
- `Yes — continue here`, description: `Move to consumer mode using the handoff payload we just built.`
|
|
208
|
+
- `No — I'll send the payload to my teammate`, description: `Stop here.`
|
|
209
|
+
|
|
210
|
+
If yes, follow `references/consumer-install.md` from step 1, pre-filling the handoff payload values.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Rollback / undo
|
|
215
|
+
|
|
216
|
+
If the operator wants to undo what was just provisioned:
|
|
217
|
+
|
|
218
|
+
1. **Revoke the key** — admin SPA at `/settings/widget-keys` → find the row → click "Révoquer". This breaks the widget immediately for anyone using it.
|
|
219
|
+
2. **Delete the project** — currently no UI button; instruct the operator to use the DO Console → backend pod → `python manage.py shell -c "from mhosaic_feedback.models import Project; Project.objects.filter(slug='<slug>').delete()"`. This cascades to keys, reports, etc. — only do this if no real reports have been created against the project.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Error catalog
|
|
224
|
+
|
|
225
|
+
| Symptom | Cause | Fix |
|
|
226
|
+
|---|---|---|
|
|
227
|
+
| `fetch` returns 401 | JWT expired | Re-do step 2 (re-login via SSO) |
|
|
228
|
+
| `fetch` returns 403 on companies POST | Not staff | Operator's user lacks `is_staff` — provision via `/admin/` or `python manage.py shell` |
|
|
229
|
+
| `fetch` returns 400 on projects POST with `allowed_origins` | Origin format invalid | Parse server error, walk operator through fixing the URL, retry |
|
|
230
|
+
| `fetch` returns 400 with `slug` collision | Slug already exists in this company | Append `-2`, confirm with operator, retry |
|
|
231
|
+
| Plaintext key not in response | Wrong endpoint (used `/api/feedback/v1/projects/<id>/keys/` instead of `/api/feedback/v1/project-keys/create/`) | Use the correct endpoint per `packages/backend/mhosaic_feedback/views_project_keys.py:36` |
|
|
232
|
+
| `mcp__claude-in-chrome__javascript_tool` returns null/undefined | Snippet didn't return a value | All snippets above end with `JSON.stringify(...)` — verify the snippet's last expression is a JSON-serializable value |
|
|
233
|
+
| Operator says "I don't see the new project" | Browser cache | Hard reload (Cmd+Shift+R). If still missing, network tab → check POST returned 201 |
|