diff --git a/.github/ISSUE_TEMPLATE/connection-problem.yml b/.github/ISSUE_TEMPLATE/connection-problem.yml
new file mode 100644
index 0000000..63969ce
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/connection-problem.yml
@@ -0,0 +1,42 @@
+name: Connection problem
+description: Your brush isn't in the list, or it won't connect.
+title: 'Connection problem: '
+labels: ['connection problem']
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Sorry it didn't work. Tell us what you have — knowing which brushes don't connect is the
+ first step to supporting them.
+
+ Tip: most brushes only advertise while awake. Press the power button (or pick the brush up)
+ right before connecting.
+ - type: input
+ id: model
+ attributes:
+ label: Brush model
+ description: As printed on the handle or box.
+ placeholder: Oral-B iO Series 7
+ validations:
+ required: true
+ - type: dropdown
+ id: problem
+ attributes:
+ label: What happened?
+ options:
+ - My brush isn't in the list
+ - It's in the list, but won't connect
+ - It connects, then drops
+ validations:
+ required: true
+ - type: textarea
+ id: details
+ attributes:
+ label: Anything else?
+ description: Did it show up under a different name? Is it paired with the vendor app on another phone?
+ - type: textarea
+ id: environment
+ attributes:
+ label: Environment
+ description: Filled in by the app. Review it before submitting.
+ render: text
diff --git a/.github/ISSUE_TEMPLATE/device-report.yml b/.github/ISSUE_TEMPLATE/device-report.yml
new file mode 100644
index 0000000..2259f08
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/device-report.yml
@@ -0,0 +1,45 @@
+name: Device report
+description: Tell us how your brush works with Brushlog — this is how models get confirmed.
+title: 'Device report: '
+labels: ['device report']
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks! Brushlog is confirmed on the Oral-B iO; older Genius / SmartSeries / Smart brushes
+ are supported from prior art but untested. Your report is how they get confirmed (or fixed).
+
+ If you opened this from the app, the **Connection** box below is already filled in with
+ your brush's model identifiers and Bluetooth layout — no brushing data. If we need more to
+ fix something, we'll ask for the in-app Diagnostics report in the thread.
+ - type: input
+ id: model
+ attributes:
+ label: Brush model
+ description: As printed on the handle or box.
+ placeholder: Oral-B iO Series 7
+ validations:
+ required: true
+ - type: dropdown
+ id: works
+ attributes:
+ label: Does it work?
+ multiple: true
+ options:
+ - Live view works (timer, mode, pressure)
+ - History sync works
+ - Brush-head wear shows up
+ - Something is wrong (tell us below)
+ validations:
+ required: true
+ - type: textarea
+ id: details
+ attributes:
+ label: What did you see?
+ description: Anything missing, wrong or odd — e.g. a mode shown under the wrong name.
+ - type: textarea
+ id: connection
+ attributes:
+ label: Connection
+ description: Filled in by the app. Review it before submitting.
+ render: text
diff --git a/README.md b/README.md
index 1f6f9d7..0dd160f 100644
--- a/README.md
+++ b/README.md
@@ -102,6 +102,13 @@ brush-head wear. Older **Genius / SmartSeries / Smart** lines are covered from p
but not tested here. The client feature-detects every characteristic, so an unknown model
connects and exposes whatever it supports — the in-app Diagnostics panel shows what that is.
+Have a brush other than an iO? When it connects, Brushlog asks how it went and opens a
+pre-filled [device report](https://github.com/libreble/brushlog/issues/new?template=device-report.yml)
+— model identifiers and Bluetooth layout, no brushing data. If your brush isn't in the list or
+won't connect, dismissing the chooser offers a
+[connection-problem report](https://github.com/libreble/brushlog/issues/new?template=connection-problem.yml).
+Nothing is sent from the app; you review and submit the issue on GitHub.
+
## Privacy
No network calls, no analytics, no accounts. Your data lives in this browser only; export it to
diff --git a/src/App.tsx b/src/App.tsx
index d6dcb6c..33a622e 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -9,6 +9,7 @@ import type { StoredSession } from './lib/session.ts';
import { ConnectBar } from './components/ConnectBar.tsx';
import { LiveSession } from './components/LiveSession.tsx';
import { DiagnosticsPanel } from './components/DiagnosticsPanel.tsx';
+import { ReportToast } from './components/ReportToast.tsx';
import { StatGrid } from './components/StatGrid.tsx';
import { TrendChart } from './components/TrendChart.tsx';
import { Insights } from './components/Insights.tsx';
@@ -104,6 +105,15 @@ export default function App() {
)}
+
+
{selected && (
setSelected(null)} goalDurationS={goalDurationS} />
)}
diff --git a/src/components/ReportToast.tsx b/src/components/ReportToast.tsx
new file mode 100644
index 0000000..bf6044e
--- /dev/null
+++ b/src/components/ReportToast.tsx
@@ -0,0 +1,103 @@
+import { useEffect, useState } from 'react';
+import type { ConnState } from '../hooks/useBrush.ts';
+import type { DeviceInfo, DiscoveredService } from '../protocol/types.ts';
+import { connectionProblemUrl, deviceReportUrl, isConfirmedModel } from '../lib/report.ts';
+
+interface Props {
+ state: ConnState;
+ cancelled: boolean;
+ error: string | null;
+ device: DeviceInfo | null;
+ discovery: DiscoveredService[] | null;
+ /** Lift above the bottom-centre "session saved" toast while that's showing. */
+ raised: boolean;
+}
+
+const doneKey = (d: DeviceInfo) => `brushlog.reportDone.${d.modelId}-${d.protocolVersion ?? 'x'}`;
+
+function isDone(d: DeviceInfo): boolean {
+ try {
+ return localStorage.getItem(doneKey(d)) !== null;
+ } catch {
+ return false;
+ }
+}
+
+/**
+ * A dismissible, non-blocking toast inviting a device report (see lib/report.ts):
+ * - a brush that isn't an iO connected → "does it work?", once per model (dismiss or report);
+ * - the chooser was dismissed or connecting failed → "not in the list, or won't connect?",
+ * hidden on dismiss until the next cancel/failure.
+ * Both open a pre-filled GitHub issue form; the user reviews and submits it there.
+ */
+export function ReportToast({ state, cancelled, error, device, discovery, raised }: Props) {
+ const [doneNow, setDoneNow] = useState(false);
+ const [problemDismissed, setProblemDismissed] = useState(false);
+ const problem = cancelled || state === 'error';
+
+ useEffect(() => {
+ if (!problem) setProblemDismissed(false);
+ }, [problem]);
+
+ // Wait for discovery so the report carries the GATT layout.
+ const ask =
+ state === 'connected' && device && discovery && !isConfirmedModel(device) && !doneNow && !isDone(device);
+
+ if (ask) {
+ const finish = () => {
+ try {
+ localStorage.setItem(doneKey(device), '1');
+ } catch { /* storage unavailable — it may ask again next time */ }
+ setDoneNow(true);
+ };
+ return (
+
+
Brushlog is only confirmed on the Oral-B iO so far. Does your brush work?
+
+ Tell us how it went
+
+
+ );
+ }
+
+ if (problem && !problemDismissed && state !== 'connected') {
+ return (
+ setProblemDismissed(true)}>
+