feat(web): wire settings feedback submission

This commit is contained in:
OpenAI Codex 2026-05-05 00:53:43 -07:00
parent 5dd0a18221
commit d38b9745a4
3 changed files with 50 additions and 19 deletions

View File

@ -17,7 +17,7 @@
| TODO | Priority | Phase | File(s) | Summary |
|------|----------|-------|---------|---------|
| **TODO-001** | medium | 0 | `web/src/app/providers.tsx` | Kill switch maintenance banner — create `<MaintenanceBanner />`, set React state when `checkKillSwitch()` returns `disabled=true`, disable timer creation buttons |
| **TODO-002** | medium | 0 | `web/src/app/(app)/settings/page.tsx` (create) | Wire feedback button into settings page (or floating FAB) using `feedbackClient` from `web/src/lib/feedback.ts` |
| **TODO-002** | medium | 0 | `web/src/app/settings/page.tsx` | ~~Wire feedback button into settings page using `feedbackClient`~~ — settings feedback form submits through `@bytelyst/feedback-client`; verification blocked locally by missing `GITEA_NPM_TOKEN` for private `@bytelyst/*` install (df670d9) |
| **TODO-003** | medium | 0 | `web/src/components/CreateTimerModal.tsx`, `web/src/components/AlarmOverlay.tsx` | Apply `@bytelyst/accessibility` focus trap + screen reader announcements. Ensure `--cm-*` color tokens meet WCAG AA contrast |
| ✅ **TODO-004** | medium | A.1 | `backend/src/modules/routines/routes.ts` | ~~Clone template~~ — templates now cloned via `crypto.randomUUID()`, original stays reusable (0e7c1ae) |
| ✅ **TODO-005** | high | A.4 | `backend/src/lib/ai-context.ts` | ~~Wire LLM~~ — dual-path: extraction-service → Ollama `/api/generate` (5s timeout, feature-gated) (229ce4f) |
@ -64,7 +64,7 @@
| `@bytelyst/design-tokens` | ✅ Used | `--cm-*` CSS props in globals.css |
| `@bytelyst/ui` | ✅ Used | Imported in providers.tsx |
| `@bytelyst/kill-switch-client` | ❌ **MISSING** | Every other product has this — critical for prod safety |
| `@bytelyst/feedback-client` | **MISSING** | No in-app feedback mechanism |
| `@bytelyst/feedback-client` | ✅ Used | Settings feedback form submits via shared SDK |
| `@bytelyst/event-store` | ❌ **MISSING** | Domain events not persisted — needed for webhooks + audit |
| `@bytelyst/webhook-dispatch` | ❌ **MISSING** | Backend uses hand-rolled dispatcher instead of shared package |
| `@bytelyst/accessibility` | ❌ **MISSING** | Shared accessibility helpers not consumed |
@ -124,7 +124,7 @@ These proxy to `chronomind-backend` (port 4011) via `chronomind-client.ts`.
- [x] `web/src/lib/feedback.ts` — integrate `@bytelyst/feedback-client` (commit: f3e14e2)
- `createFeedbackClient({ baseUrl, productId, getAccessToken })`
- [ ] Add feedback button to settings page (or floating FAB) (commit: )
- [x] Add feedback button to settings page (or floating FAB) (commit: df670d9; status: implemented, verification blocked locally because `pnpm install` requires `GITEA_NPM_TOKEN` for private `@bytelyst/*` packages)
- TODO-002: Wire feedback button into settings page using `feedbackClient` from `web/src/lib/feedback.ts`
### 0.3 — Accessibility Package (Web)
@ -642,7 +642,7 @@ Systematic audit against `learning_ai_common_plat/packages/` (58 packages) revea
| Gap | Severity | Fixed In |
|-----|----------|----------|
| No `@bytelyst/kill-switch-client` | **Critical** | Phase 0.1 |
| No `@bytelyst/feedback-client` | Medium | Phase 0.2 |
| No settings feedback entry point using `@bytelyst/feedback-client` | Medium | Phase 0.2 — complete (df670d9) |
| No `@bytelyst/accessibility` helpers | Medium | Phase 0.3 |
| No feature flags for new features | **Critical** | Phase 0.4 |
| No telemetry events for new features | Medium | Phase 0.5 |

View File

@ -38,6 +38,7 @@ export default function SettingsPage() {
const [feedbackText, setFeedbackText] = useState('');
const [feedbackSubmitting, setFeedbackSubmitting] = useState(false);
const [feedbackSuccess, setFeedbackSuccess] = useState(false);
const [feedbackError, setFeedbackError] = useState('');
useEffect(() => {
setMounted(true);
@ -106,6 +107,38 @@ export default function SettingsPage() {
.forEach((t) => removeTimer(t.id));
};
const handleFeedbackSubmit = async () => {
const body = feedbackText.trim();
if (!body) return;
setFeedbackSubmitting(true);
setFeedbackError('');
try {
await feedbackClient.submitWithScreenshot({
type: feedbackType,
title: body.slice(0, 100),
body,
screen: 'settings',
appVersion: '0.1.0',
platform: 'web',
deviceContext: {
osVersion: navigator.userAgent,
appVersion: '0.1.0',
deviceModel: navigator.platform || 'web',
screenResolution: `${window.screen.width}x${window.screen.height}`,
locale: navigator.language,
},
});
setFeedbackSuccess(true);
setFeedbackText('');
setTimeout(() => setFeedbackSuccess(false), 4000);
} catch {
setFeedbackError('Feedback could not be sent right now. Please try again in a moment.');
} finally {
setFeedbackSubmitting(false);
}
};
return (
<div className="min-h-screen" style={{ backgroundColor: 'var(--cm-bg-canvas)' }}>
<div className="max-w-2xl mx-auto px-4 py-8">
@ -526,8 +559,12 @@ export default function SettingsPage() {
))}
</div>
<textarea
aria-label="Feedback message"
value={feedbackText}
onChange={(e) => setFeedbackText(e.target.value)}
onChange={(e) => {
setFeedbackText(e.target.value);
setFeedbackError('');
}}
placeholder="Tell us what you think..."
rows={3}
className="w-full px-3 py-2 rounded-lg text-sm outline-none resize-none"
@ -537,21 +574,15 @@ export default function SettingsPage() {
border: '1px solid var(--cm-border)',
}}
/>
{feedbackError && (
<p className="text-xs" style={{ color: 'var(--cm-danger)' }} role="alert">
{feedbackError}
</p>
)}
<button
onClick={async () => {
if (!feedbackText.trim()) return;
setFeedbackSubmitting(true);
try {
await feedbackClient.submitWithScreenshot({ type: feedbackType, title: feedbackText.trim().slice(0, 100), body: feedbackText.trim(), platform: 'web' });
setFeedbackSuccess(true);
setFeedbackText('');
setTimeout(() => setFeedbackSuccess(false), 4000);
} catch {
// fail silently — feedback is best-effort
}
setFeedbackSubmitting(false);
}}
onClick={handleFeedbackSubmit}
disabled={feedbackSubmitting || !feedbackText.trim()}
aria-label="Send feedback"
className="w-full px-4 py-2 rounded-lg text-sm font-medium cursor-pointer disabled:opacity-40"
style={{ backgroundColor: 'var(--cm-accent)', color: 'var(--cm-white)' }}
>

View File

@ -11,7 +11,7 @@ import { createFeedbackClient } from '@bytelyst/feedback-client';
import { getAuthClient, getBaseUrl } from './auth-api';
const feedbackClient = createFeedbackClient({
baseUrl: `${getBaseUrl()}/api`,
baseUrl: getBaseUrl(),
getAuthToken: () => getAuthClient().getAccessToken(),
});