Configuration#
How settings are layered#
Three layers, lowest precedence first:
- Built-in defaults —
DEFAULTSinapp/config.py. The app runs with no config file at all. app/config/config.yml— your settings. If it does not exist,config.example.ymlis used instead, so a fresh checkout boots.- 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.