georgeandClaude Opus 5 eae889c52b Add the Verbs tab, and stop Daily rounds repeating a preposition
The possessive and pronoun levels already build sentences with no
preposition in them - "Ich kenne ___ Mann", "Ich helfe ___ Mann" - where
the case is decided purely by the verb. Six verbs were doing that silently
across seven levels (sehen, kennen, lieben, hoeren as accusative, helfen as
dative, geben as both) and nothing in the app had ever taught or tested it.

The framing the whole section rests on: there is no accusative list to
learn. Accusative is the default for hundreds of transitive verbs, so you
learn the dative exceptions - the same shape as the preposition lists the
app already teaches.

39 verbs of data, in four groups:
- accusative foils, including the ones English makes feel dative
  (fragen, anrufen, treffen, unterstuetzen, moegen)
- 12 dative verbs: helfen, danken, antworten, glauben, folgen, gratulieren,
  vertrauen, verzeihen, begegnen, zuhoeren, raten, schaden
- 4 that flip subject and object against English: gefallen, schmecken,
  fehlen, gehoeren
- 8 two-object verbs, all covered by one rule: person Dativ, thing Akkusativ

Six lessons plus Daily Practice: Welcher Fall? / Die Fallen / Gefallen & Co.
/ Dativ + Akkusativ / Im Satz / Alles gemischt. The tab sits second, right
after Prepositions, so case government is learned before the forms that
depend on it.

Near-synonyms that disagree are modelled as a `trap` link, and a test
asserts every trap points at a real verb taking the other case and that the
link is symmetric - so fragen/antworten, treffen/begegnen, anrufen/zuhoeren,
unterstuetzen/helfen and moegen/gefallen cannot drift apart.

The inverted verbs carry an explicit English reordering rule rather than a
literal gloss, so "Das Fenster gefaellt der Aerztin" renders as "The doctor
likes the window" and never "The window pleases the doctor". Tested.

Verb sentences are enumerated into corpus.txt like everything else, keyed on
verb x noun, which is what caught "I lend the teacher the bed" and "I
recommend the girl the cupboard" before they shipped - two-object verbs now
name the things that can actually be handed over, and the English uses the
"to the person" order.

167 tests pass.

Known gap: a handful of verbs take the genitive (gedenken, beduerfen,
beschuldigen). They are formal and rare, so the rule panel mentions them and
the app does not drill them.

Daily rounds could serve the same preposition twice, even back-to-back

Reported as "für" twice in a row in the Prepositions daily. Measured before
the fix: 14% of prep-daily rounds had a back-to-back repeat and 51% repeated
some preposition.

A Daily round mixes several lessons, and each one was handed its own
newState(). The anti-repeat memory therefore only spanned one lesson's slice
of the round, so level 8 drawing "für" and level 10 drawing "für" could not
see each other. drawBag also only avoided the previous item when the bag
refilled, not on every draw.

- newState(shared) lets several lesson states run off one memory, and the
  Daily hands the same one to every lesson it mixes.
- drawBag now skips anything already used this cycle, and never repeats the
  previous item, checked on every draw rather than only at a refill.
- Kontraktionen drew through a different bag ('contr') and so bypassed the
  preposition memory entirely, letting "im" follow "in". It now consults and
  records the shared memory.
- Adjective frames were chosen with a plain pick(), so a Daily could repeat
  a whole carrier sentence. They are dealt from the shared memory too.

Rejected draws are put BACK into the bag rather than discarded. The first
version burned them, which skewed the Kontraktionen deal 1.5x toward the
forms whose preposition is not shared with a sibling form; reinsertion holds
the spread at 1.04x.

After: every Daily and every preposition-drilling level is at 0% for both
back-to-back and within-round repeats. Two cases remain non-zero and are
arithmetic rather than bugs - a round is 10 questions, so Wechsel im Kontext
(9 prepositions) must repeat one, and the Stark adjective level has exactly
one prepositional carrier frame. The adjective tab is held to frame variety
rather than preposition variety, since there the preposition is scenery and
the ending is the question.

Tests pin all of it: no Daily may repeat a preposition, no level may repeat
one back-to-back where it has a choice, and no Daily may repeat a carrier
sentence back-to-back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Uza6mhTpAvSwMQuFc4Cz8
2026-10-05 06:12:27 +00:00
2026-07-05 12:48:27 +02:00
2026-07-05 12:48:27 +02:00

Deklinationstrainer

A daily-practice German declension trainer. Self-hosted so your progress actually saves.

Deploy to Coolify

One-time setup

  1. Push these files to a git repo (GitHub / Gitea / whatever Coolify is connected to).

  2. 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)
  3. Add a domain (e.g. deklination.yourdomain.com). Coolify provisions HTTPS via Caddy/Traefik automatically.

  4. 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.99 every week
  • Yearly subscription: $9.99 every year
  • Lifetime access: $19.99 one-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.
S
Description
No description provided
Readme
4 MiB
Languages
JavaScript 99.7%
HTML 0.2%