feat(web): wire settings feedback submission
This commit is contained in:
parent
5dd0a18221
commit
d38b9745a4
@ -17,7 +17,7 @@
|
|||||||
| TODO | Priority | Phase | File(s) | Summary |
|
| 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-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-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-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) |
|
| ✅ **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/design-tokens` | ✅ Used | `--cm-*` CSS props in globals.css |
|
||||||
| `@bytelyst/ui` | ✅ Used | Imported in providers.tsx |
|
| `@bytelyst/ui` | ✅ Used | Imported in providers.tsx |
|
||||||
| `@bytelyst/kill-switch-client` | ❌ **MISSING** | Every other product has this — critical for prod safety |
|
| `@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/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/webhook-dispatch` | ❌ **MISSING** | Backend uses hand-rolled dispatcher instead of shared package |
|
||||||
| `@bytelyst/accessibility` | ❌ **MISSING** | Shared accessibility helpers not consumed |
|
| `@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)
|
- [x] `web/src/lib/feedback.ts` — integrate `@bytelyst/feedback-client` (commit: f3e14e2)
|
||||||
- `createFeedbackClient({ baseUrl, productId, getAccessToken })`
|
- `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`
|
- TODO-002: Wire feedback button into settings page using `feedbackClient` from `web/src/lib/feedback.ts`
|
||||||
|
|
||||||
### 0.3 — Accessibility Package (Web)
|
### 0.3 — Accessibility Package (Web)
|
||||||
@ -642,7 +642,7 @@ Systematic audit against `learning_ai_common_plat/packages/` (58 packages) revea
|
|||||||
| Gap | Severity | Fixed In |
|
| Gap | Severity | Fixed In |
|
||||||
|-----|----------|----------|
|
|-----|----------|----------|
|
||||||
| No `@bytelyst/kill-switch-client` | **Critical** | Phase 0.1 |
|
| 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 `@bytelyst/accessibility` helpers | Medium | Phase 0.3 |
|
||||||
| No feature flags for new features | **Critical** | Phase 0.4 |
|
| No feature flags for new features | **Critical** | Phase 0.4 |
|
||||||
| No telemetry events for new features | Medium | Phase 0.5 |
|
| No telemetry events for new features | Medium | Phase 0.5 |
|
||||||
|
|||||||
@ -38,6 +38,7 @@ export default function SettingsPage() {
|
|||||||
const [feedbackText, setFeedbackText] = useState('');
|
const [feedbackText, setFeedbackText] = useState('');
|
||||||
const [feedbackSubmitting, setFeedbackSubmitting] = useState(false);
|
const [feedbackSubmitting, setFeedbackSubmitting] = useState(false);
|
||||||
const [feedbackSuccess, setFeedbackSuccess] = useState(false);
|
const [feedbackSuccess, setFeedbackSuccess] = useState(false);
|
||||||
|
const [feedbackError, setFeedbackError] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
@ -106,6 +107,38 @@ export default function SettingsPage() {
|
|||||||
.forEach((t) => removeTimer(t.id));
|
.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 (
|
return (
|
||||||
<div className="min-h-screen" style={{ backgroundColor: 'var(--cm-bg-canvas)' }}>
|
<div className="min-h-screen" style={{ backgroundColor: 'var(--cm-bg-canvas)' }}>
|
||||||
<div className="max-w-2xl mx-auto px-4 py-8">
|
<div className="max-w-2xl mx-auto px-4 py-8">
|
||||||
@ -526,8 +559,12 @@ export default function SettingsPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
|
aria-label="Feedback message"
|
||||||
value={feedbackText}
|
value={feedbackText}
|
||||||
onChange={(e) => setFeedbackText(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setFeedbackText(e.target.value);
|
||||||
|
setFeedbackError('');
|
||||||
|
}}
|
||||||
placeholder="Tell us what you think..."
|
placeholder="Tell us what you think..."
|
||||||
rows={3}
|
rows={3}
|
||||||
className="w-full px-3 py-2 rounded-lg text-sm outline-none resize-none"
|
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)',
|
border: '1px solid var(--cm-border)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{feedbackError && (
|
||||||
|
<p className="text-xs" style={{ color: 'var(--cm-danger)' }} role="alert">
|
||||||
|
{feedbackError}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={handleFeedbackSubmit}
|
||||||
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);
|
|
||||||
}}
|
|
||||||
disabled={feedbackSubmitting || !feedbackText.trim()}
|
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"
|
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)' }}
|
style={{ backgroundColor: 'var(--cm-accent)', color: 'var(--cm-white)' }}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { createFeedbackClient } from '@bytelyst/feedback-client';
|
|||||||
import { getAuthClient, getBaseUrl } from './auth-api';
|
import { getAuthClient, getBaseUrl } from './auth-api';
|
||||||
|
|
||||||
const feedbackClient = createFeedbackClient({
|
const feedbackClient = createFeedbackClient({
|
||||||
baseUrl: `${getBaseUrl()}/api`,
|
baseUrl: getBaseUrl(),
|
||||||
getAuthToken: () => getAuthClient().getAccessToken(),
|
getAuthToken: () => getAuthClient().getAccessToken(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user