Configuration#

How settings are layered#

Three layers, lowest precedence first:

  1. Built-in defaultsDEFAULTS in app/config.py. The app runs with no config file at all.
  2. app/config/config.yml — your settings. If it does not exist, config.example.yml is used instead, so a fresh checkout boots.
  3. Environment variables (RT_*) — these win, and they cover the things that differ per deployment: ports, secrets, the database URL.

YAML layers are deep-merged, so you only need to write the keys you want to change. Setting targets.default_goal does not wipe targets.splits.

The merged result is available to the app as app.config["RT"] and to templates as settings.

cp app/config/config.example.yml app/config/config.yml
$EDITOR app/config/config.yml
docker compose restart web

Config is read once, at startup

Unlike the documentation tree, the config file is parsed when the process starts and cached. Restart the container after editing it.

Environment variables#

Set these in .env, which Compose passes into the container.

Variable Default What it does
RT_HTTP_PORT 8080 Host port published by Compose. The container always listens on 8000
RT_SECRET_KEY dev-only-insecure-key Flask session signing key. Change it. The session cookie is what remembers your active profile
RT_ENV production Free-form environment label, exposed as app.config["RT_ENV"]
RT_DATABASE_URL sqlite:////data/rubiks-trainer.db SQLAlchemy URL. Any SQLAlchemy-supported database works; SQLite and Postgres are what we test
RT_CONFIG_FILE app/config/config.yml Absolute path to the YAML config inside the container. Compose sets it to /app/app/config/config.yml

Note the four slashes in the SQLite URL: sqlite:////data/... is an absolute path, sqlite:///data/... is relative to the working directory. Getting this wrong is the most common cause of "my data disappeared" — see Troubleshooting.

config.yml#

app#

Key Default What it does
app.name Rubik's Trainer Shown in the header, the page titles and the footer
app.tagline CFOP practice for sub-20 solvers Sub-line under the brand and the HTML meta description
app.max_content_width_vw 90 Percentage of the viewport the main container may use. Matches --content-max in base.css

scrambler#

Controls full scramble generation. See Scrambling.

Key Default What it does
scrambler.base_twists 18 Base number of random face turns
scrambler.extra_twists_max 6 Up to this many further turns are appended, so length is 18–24
scrambler.forbid_same_face_repeat true Never emit two moves on the same face in a row (R R' would cancel)
scrambler.forbid_parallel_sandwich true Never emit A B A on parallel faces (R L R reduces to two moves)

Turning either rule off produces scrambles that are shorter than they look. Do not, unless you are testing the parser.

drills#

See Drills.

Key Default What it does
drills.weakest_case_count 3 How many of the weakest cases go into a generated session
drills.default_reps 5 Repetitions per case in a session
drills.inspection_seconds 0 WCA-style inspection countdown before each timed rep. 0 disables it

Raising weakest_case_count above about 6 defeats the point of the feature — the value of a small set is that you meet the same case again while the last rep is still fresh.

targets#

The heart of the scoring model. See Expected times for where these numbers come from and how much to trust them.

Key Default What it does
targets.splits five goals, see below Per-goal phase target times in seconds
targets.default_goal sub_20 Goal assigned to a new profile
targets.min_attempts_for_measured 3 Timed attempts needed before measured times replace the self-assessment in a case's score

Each entry under targets.splits is keyed by the goal name used on profiles (Profile.goal_key) and holds a label plus one float per phase:

targets:
  splits:
    sub_20:
      label: "Sub-20"
      cross: 2.5
      f2l: 9.5
      oll: 4.0
      pll: 4.0
Goal Label Cross F2L OLL PLL
sub_30 Sub-30 4.0 15.0 5.5 5.5
sub_20 Sub-20 2.5 9.5 4.0 4.0
sub_15 Sub-15 2.0 7.0 3.0 3.0
sub_12 Sub-12 1.5 5.5 2.5 2.5
sub_10 Sub-10 1.2 4.6 2.2 2.0

You can add your own goal — a sub_8 row, or a casual row with generous targets — and it appears in the profile goal picker automatically. Every phase key in PHASES (cross, f2l, oll, pll) should be present; a missing phase falls back to the default goal's value.

Lowering min_attempts_for_measured to 1 makes scores react immediately and also makes them jumpy, since a single lucky rep then defines a case. Three is the smallest number that lets the trimmed mean do anything useful.

assessment#

Key Default What it does
assessment.scale six labels, 0–5 The self-assessment scale shown in the wizard
assessment:
  scale:
    0: "Never seen it"
    1: "Recognise it, cannot solve it"
    2: "Solve it slowly, need to think"
    3: "Solve it, some hesitation"
    4: "Solid, smooth"
    5: "Instant, no thought"

You can reword the labels — for a younger student, or in another language — but do not change the range. 0..5 is baked into the score formula (self_score / 5 × 100, capped at 80) and into the Assessment.score column. Keys arriving from YAML as strings are normalised to integers on load.

viewer#

See The 3D viewer.

Key Default What it does
viewer.default_speed 2.0 Playback speed in moves per second
viewer.speed_min 0.25 Slowest the speed control goes
viewer.speed_max 8.0 Fastest the speed control goes
viewer.colors standard scheme Facelet colours, keyed by face letter, plus core
viewer:
  colors:
    U: "#f7f7f7"   # white
    D: "#ffd500"   # yellow
    F: "#009b48"   # green
    B: "#0045ad"   # blue
    R: "#b71234"   # red
    L: "#ff5800"   # orange
    core: "#1b1d21"

This is the standard western (BOY) scheme. Changing it affects the 3D viewer, the flat net and every case thumbnail, because all three read the same map. A common change is swapping F/B and R/L for the Japanese scheme.

The keys are face letters, not colour names, because the internal cube state is colour-agnostic — a facelet stores which face it belongs to, and colour is applied at render time.

docs#

Key Default What it does
docs.root docs Directory rendered at /docs. Relative paths resolve from the repository root

Point this somewhere else and /docs serves that tree instead. Only *.md files are served, and only from inside the root — anything resolving outside it (including through a symlink) returns 404.

A minimal config.yml#

You do not need the whole example file. This is a complete, valid config:

app:
  name: "Cube Club"

targets:
  default_goal: sub_15

drills:
  weakest_case_count: 5
  inspection_seconds: 15

Everything else falls through to the defaults.

Checking what is loaded#

docker compose run --rm web python -c \
  "from app.config import load_settings; import json; print(json.dumps(load_settings(), indent=2))"

That prints the fully merged settings, which is the fastest way to find out whether your file is being read at all. If it looks like the defaults, check RT_CONFIG_FILE and that the file is actually mounted into the container.

Source: docs/admin/configuration.md