Reported as "not a single dative in the ein-Wörter section". The engine did
still produce dative questions, but only from 6 of the 9 dative
prepositions, so the dative was thinner than the lesson claimed and leant
on Wechsel preps landing on dative rather than on dative preps themselves.
The indefOnly frame filter was throwing three away:
- bei — a regression from the "bei der Schwester" fix. That commit added
`!f.enPossS` to the filter, which killed bei's only non-possessive frame
outright. "bei einem Arzt" is perfectly good German and reads as "at a
doctor's" in English, so there was never a reason to exclude it. von and
zu survived but lost their person frames the same way, halving them.
- nach, seit — excluded as "definite idioms" because their frames carry an
enDet override ("nach der Schule" is "after school", not "after the
school"). But "nach einer Woche" and "seit einer Woche" are textbook
German; the engine's own PREPS entry for seit uses "seit einer Woche" as
its example.
The enDet exclusion existed to stop "während einer Woche", the sentence
reported a while back, so it could not simply be deleted. Frames can now
name `indefNouns`: the nouns that DO work once an ein-word is in front,
overriding the blanket noIndef flag for that frame only. nach and seit
re-admit Woche; während re-admits nothing, so "während einer Woche" stays
impossible. seit also gets enPrepIndef, because "seit einer Woche" is "FOR
a week" in English, not "since a week".
trotz stays out, but honestly: it only governs uncountables here ("trotz
eines Wetters" is not German), so it is removed from level 7's preps list
rather than left advertised and unreachable — the same treatment zu got in
the Dativ lesson.
The guard that should have caught this: a new test asserts that every level
generates every preposition it lists. A prep that can never be asked about
is now a test failure, not a silent hole. Two older tests that asserted the
buggy behaviour (that nach/seit never appear, that no noIndef noun ever
appears) are rewritten to assert the actual rule instead of being loosened.
Dative coverage in ein-Wörter: 6/9 prepositions before, 9/9 after. Case mix
47% dative / 32% accusative / 21% genitive. A round of 10 with no dative at
all went from 0.2% to 0.06%.
142 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Uza6mhTpAvSwMQuFc4Cz8
Deklinationstrainer
A daily-practice German declension trainer. Self-hosted so your progress actually saves.
Deploy to Coolify
One-time setup
-
Push these files to a git repo (GitHub / Gitea / whatever Coolify is connected to).
-
In Coolify, create a new Application:
- Source: your git repo
- Build pack: Nixpacks (auto-detects Vite) or Static
- Build command:
npm run build - Publish directory:
dist - Port: not needed for static output (Coolify serves it directly)
-
Add a domain (e.g.
deklination.yourdomain.com). Coolify provisions HTTPS via Caddy/Traefik automatically. -
Deploy. Visit the URL on any device.
Local dev
npm install
npm run dev # http://localhost:5173
npm run build # outputs to ./dist
On iPhone
Open the URL in Safari, tap Share → Add to Home Screen. It opens fullscreen like a native app, and progress saves in localStorage across reboots, sessions, and app closes.
Storage
The app tries window.storage first (Claude artifact environment), then falls back to localStorage. When self-hosted, only localStorage runs, which iOS Safari persists reliably.
To wipe progress: open browser devtools → Application → Local Storage → delete the gd-v10 key. Or on iPhone: Settings → Safari → Advanced → Website Data → search your domain → delete.
Stripe checkout
Beug uses public Stripe Checkout or Payment Link URLs, supplied through environment variables. Do not put Stripe secret keys in this Vite app.
Payments are behind a launch-mode flag:
VITE_PAYMENTS_ENABLED=false
When VITE_PAYMENTS_ENABLED is absent or false, the app runs in Reddit/free mode: all lessons are usable, paid rows are not blurred, and the paywall/pricing screen is hidden. The Stripe URLs can stay configured in the environment so billing can be switched back on later with one env change.
To turn payments back on, set:
VITE_PAYMENTS_ENABLED=true
Create one product in Stripe, for example Beug Premium, with three prices:
- Weekly subscription:
$2.99every week - Yearly subscription:
$9.99every year - Lifetime access:
$19.99one-time payment
Create a Payment Link or Checkout link for each price. Set the success redirect URL to:
https://beug.george1.dev/?checkout=success
Set the cancel URL to:
https://beug.george1.dev/
Then add these public URLs in the deployment environment:
VITE_STRIPE_YEARLY_URL=https://buy.stripe.com/...
VITE_STRIPE_LIFETIME_URL=https://buy.stripe.com/...
VITE_STRIPE_WEEKLY_URL=https://buy.stripe.com/...
The current v1 unlock is intentionally simple: after Stripe redirects back with ?checkout=success, Beug stores the unlock in this browser's localStorage. This avoids accounts for the tiny-app version, but it is a soft paywall. A stricter version needs a small backend, Stripe webhooks, and account or email-based entitlement lookup.
Future prompt to re-enable payments
Turn payments back on for Beug. Set VITE_PAYMENTS_ENABLED=true in Coolify, keep the three existing Stripe Payment Link env vars, redeploy the app, and verify that unpaid users see the soft paid lesson blur, that charts remain visible, and that tapping a paid lesson opens the pricing screen with Weekly, Yearly, and Lifetime plans.