Fiks: slagmåling ga alltid 0m, delinger forsvant stille
Produksjonsbug rapportert av bruker rett etter forrige utrulling: "den målte aldri mer enn 0 meter. Jeg så ingen bilder." Rotårsak 1: "end"-steget (ballens posisjon) avfyrte GPS-målingen automatisk idet steget ble aktivt, uten tid for brukeren til faktisk å gå fra utslagsstedet til ballen -- start/slutt endte på samme sted, samme øyeblikk. Fjernet auto-avfyringen, krever nå et eksplisitt "Jeg er ved ballen nå"-trykk. Rotårsak 2 (konsekvens av #1): en 0m-avstand avvist av backendens distance_meters > 0-validering (422) gjorde at submitShot() bare lukket arket stille, ingen feilmelding. Lagt til submitError/submitting-state, parser både appens {detail:{message}} og FastAPI/Pydantic sin rå {detail:[{msg}]}-valideringsform (bekreftet eksakt hvilken ved å trigge en ekte 422), viser feilen i arket, holder det åpent ved feil. Scratch-verifisert med navigator.geolocation midlertidig overstyrt (ekte GPS-tillatelse permanent denied i CDP-automatiserte nettleser- sesjoner): "Jeg er ved ballen nå" vises korrekt, et ekte gap ga "130 m" (ikke 0) og delingen postet riktig, og en 0m-innsending viser nå feilmeldingen i stedet for å lukke stille. Se CHANGELOG.md punkt 49.
This commit is contained in:
parent
70f6e92726
commit
8da2423942
3 changed files with 105 additions and 59 deletions
|
|
@ -2065,6 +2065,8 @@ function ShotMeasurementEntry({
|
|||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [count, setCount] = useState(0)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [submitError, setSubmitError] = useState<string | null>(null)
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
fetch(urlBase, { credentials: "include" })
|
||||
|
|
@ -2078,6 +2080,11 @@ function ShotMeasurementEntry({
|
|||
}
|
||||
}, [urlBase])
|
||||
|
||||
// Reell bug funnet 2026-08-08: en avvist innsending (f.eks. serverens
|
||||
// distance_meters > 0-sjekk, hvis brukeren rakk å målt begge punkter for
|
||||
// fort) lukket tidligere arket STILLE -- ingen feilmelding, brukeren så
|
||||
// aldri hvorfor ingenting ble lagret/delt. Viser nå en reell feil og
|
||||
// holder arket åpent i stedet for å anta suksess.
|
||||
async function submitShot(result: {
|
||||
club: string
|
||||
distanceMeters: number
|
||||
|
|
@ -2089,6 +2096,9 @@ function ShotMeasurementEntry({
|
|||
share: boolean
|
||||
shareText: string
|
||||
}) {
|
||||
setSubmitError(null)
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const res = await fetch(urlBase, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
|
@ -2104,7 +2114,15 @@ function ShotMeasurementEntry({
|
|||
}),
|
||||
})
|
||||
if (!res.ok) {
|
||||
setOpen(false)
|
||||
const body = await res.json().catch(() => null)
|
||||
const detail = body?.detail
|
||||
const message =
|
||||
typeof detail === "string"
|
||||
? detail
|
||||
: typeof detail?.message === "string"
|
||||
? detail.message
|
||||
: "Kunne ikke lagre slaget. Sjekk at avstanden er gyldig (over 0 meter) og prøv igjen."
|
||||
setSubmitError(message)
|
||||
return
|
||||
}
|
||||
const shot = await res.json()
|
||||
|
|
@ -2118,6 +2136,11 @@ function ShotMeasurementEntry({
|
|||
}
|
||||
setCount((n) => n + 1)
|
||||
setOpen(false)
|
||||
} catch {
|
||||
setSubmitError("Klarte ikke å nå serveren. Sjekk tilkoblingen og prøv igjen.")
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (readOnly) return null
|
||||
|
|
@ -2130,6 +2153,7 @@ function ShotMeasurementEntry({
|
|||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setSubmitError(null)
|
||||
setOpen(true)
|
||||
}}
|
||||
className={cn(
|
||||
|
|
@ -2147,6 +2171,8 @@ function ShotMeasurementEntry({
|
|||
holeNumber={holeNumber}
|
||||
existingShotCount={count}
|
||||
ownBagClubs={ownBagClubs}
|
||||
submitError={submitError}
|
||||
submitting={submitting}
|
||||
onSubmit={submitShot}
|
||||
onCancel={() => setOpen(false)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -39,6 +39,11 @@ export type ShotMeasurementSheetProps = {
|
|||
* stub is used purely for display.
|
||||
*/
|
||||
distanceMeters?: number
|
||||
/** Vises som en feilmelding i resultat-steget, f.eks. når serveren avviste
|
||||
* innsendingen (ADR-048: reell bug 2026-08-08 -- uten dette lukket arket
|
||||
* seg stille ved en avvist innsending, ingen feil synlig for brukeren). */
|
||||
submitError?: string | null
|
||||
submitting?: boolean
|
||||
onSubmit: (result: {
|
||||
club: string
|
||||
distanceMeters: number
|
||||
|
|
@ -73,6 +78,8 @@ export function ShotMeasurementSheet({
|
|||
existingShotCount,
|
||||
ownBagClubs,
|
||||
distanceMeters,
|
||||
submitError,
|
||||
submitting,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: ShotMeasurementSheetProps) {
|
||||
|
|
@ -116,14 +123,12 @@ export function ShotMeasurementSheet({
|
|||
}
|
||||
|
||||
// Ball position is ALWAYS GPS, regardless of how the start was chosen.
|
||||
const endRequested = useRef(false)
|
||||
useEffect(() => {
|
||||
if (step !== "end") {
|
||||
endRequested.current = false
|
||||
return
|
||||
}
|
||||
if (endRequested.current) return
|
||||
endRequested.current = true
|
||||
// Deliberately NOT auto-fired on step mount (real bug found 2026-08-08:
|
||||
// firing immediately gave the user zero time to actually walk from the
|
||||
// start point to the ball, so start/end ended up at ~the same spot and
|
||||
// ~the same instant -- distance was always 0m). Requires an explicit
|
||||
// "Jeg er ved ballen nå" tap once the user has actually walked there.
|
||||
function measureEndPoint() {
|
||||
setEndStatus("loading")
|
||||
acquirePosition(
|
||||
(p) => {
|
||||
|
|
@ -133,7 +138,7 @@ export function ShotMeasurementSheet({
|
|||
},
|
||||
() => setEndStatus("error"),
|
||||
)
|
||||
}, [step])
|
||||
}
|
||||
|
||||
const distance =
|
||||
typeof distanceMeters === "number" ? distanceMeters : previewDistance(startPoint, endPoint)
|
||||
|
|
@ -145,18 +150,6 @@ export function ShotMeasurementSheet({
|
|||
}
|
||||
}, [step, club, distance, holeNumber])
|
||||
|
||||
function retryEnd() {
|
||||
setEndStatus("loading")
|
||||
acquirePosition(
|
||||
(p) => {
|
||||
setEndPoint(p)
|
||||
setEndStatus("idle")
|
||||
setStep("club")
|
||||
},
|
||||
() => setEndStatus("error"),
|
||||
)
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
if (step === "map" || step === "end") {
|
||||
setStartStatus("idle")
|
||||
|
|
@ -303,6 +296,22 @@ export function ShotMeasurementSheet({
|
|||
|
||||
{step === "end" ? (
|
||||
<div className="mx-auto flex max-w-md flex-col items-center gap-4 pt-10 text-center">
|
||||
{endStatus === "idle" ? (
|
||||
<>
|
||||
<p className="text-base text-muted-foreground text-pretty">
|
||||
Gå til der ballen ligger, og trykk når du er fremme.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={measureEndPoint}
|
||||
className="inline-flex min-h-14 w-full items-center justify-center gap-2 rounded-xl bg-primary px-6 text-lg font-bold text-primary-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<Crosshair className="size-5 shrink-0" aria-hidden="true" />
|
||||
Jeg er ved ballen nå
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{endStatus === "loading" ? (
|
||||
<>
|
||||
<div className="size-8 animate-spin rounded-full border-4 border-primary/20 border-t-primary" />
|
||||
|
|
@ -320,7 +329,7 @@ export function ShotMeasurementSheet({
|
|||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={retryEnd}
|
||||
onClick={measureEndPoint}
|
||||
className="inline-flex min-h-11 items-center justify-center gap-2 rounded-xl bg-primary px-4 text-base font-bold text-primary-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<Crosshair className="size-5" aria-hidden="true" />
|
||||
|
|
@ -410,6 +419,12 @@ export function ShotMeasurementSheet({
|
|||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{submitError ? (
|
||||
<p role="alert" className="text-base text-destructive text-pretty">
|
||||
{submitError}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</main>
|
||||
|
|
@ -430,11 +445,16 @@ export function ShotMeasurementSheet({
|
|||
) : (
|
||||
<button
|
||||
type="button"
|
||||
disabled={submitting}
|
||||
onClick={confirmSubmit}
|
||||
className="inline-flex min-h-14 w-full items-center justify-center gap-2 rounded-xl bg-primary px-6 text-lg font-bold text-primary-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
className="inline-flex min-h-14 w-full items-center justify-center gap-2 rounded-xl bg-primary px-6 text-lg font-bold text-primary-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{submitting ? (
|
||||
<div className="size-5 shrink-0 animate-spin rounded-full border-2 border-primary-foreground/30 border-t-primary-foreground" />
|
||||
) : (
|
||||
<Check className="size-5 shrink-0" aria-hidden="true" />
|
||||
Lagre slag
|
||||
)}
|
||||
{submitting ? "Lagrer …" : "Lagre slag"}
|
||||
</button>
|
||||
)}
|
||||
</footer>
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue