| name | ogp-config-and-flags |
| description | Catalog of every configuration axis in Open Garden Planner: QSettings keys (AppSettings), UI-state persistence (UiStateStore), environment variables (.env / OGP_GOOGLE_MAPS_KEY / QT_QPA_PLATFORM / PYTHONUTF8), project-file version + per-plan settings, hidden-feature toggles in code, tooling config (pyproject/ruff/mypy/pytest/pins/ogp.spec), and CI knobs. Load this when: adding or changing a setting or flag; wondering what a settings key does or what its default is; configuring environment variables or .env; touching the Preferences dialog; deciding whether a change needs a FILE_VERSION bump; or a feature seems mysteriously disabled (hidden panel, missing API key, default-off snap mode). Includes the "how to add a setting" checklist.
|
OGP Configuration & Flags Catalog
Verified 2026-07-03 against v1.23.0 by reading settings.py, ui_state.py,
project.py, main.py, application.py, preferences_dialog.py,
pyproject.toml, installer/ogp.spec, ci.yml, release.yml. Flags drift —
if the repo has moved on, re-verify with the commands at the bottom before
trusting a row.
When NOT to use this skill: build/run/test commands (ogp-build-and-run),
what a subsystem does (ogp-architecture-contract, ogp-qt-cad-reference,
ogp-garden-domain-reference), release/branch/PR process (ogp-change-control),
debugging technique (ogp-debugging-playbook, debug-verbose).
Jargon, defined once:
- QSettings — Qt's per-user persistent key/value store. This app constructs
it as
QSettings("cofade", "Open Garden Planner"), so it lives at:
Windows registry HKEY_CURRENT_USER/Software/cofade/Open Garden Planner,
macOS ~/Library/Preferences/com.cofade.Open Garden Planner.plist,
Linux ~/.config/cofade/Open Garden Planner.conf.
- App setting vs per-plan setting — app settings (QSettings) follow the
user across all plans; per-plan settings live inside the
.ogp JSON file
(ProjectData) and follow the document.
- Additive
.ogp key — a new top-level key in the project JSON that old
builds silently ignore on load. Additive keys do NOT bump FILE_VERSION.
1. AppSettings — QSettings catalog
Source: src/open_garden_planner/app/settings.py (all keys enumerated below —
the file has no others as of v1.23.0). Access via the singleton
get_settings(); each key has a typed property with the default baked in.
All rows production unless noted.
General / startup
| Key | Property | Type | Default | Read by |
|---|
autosave/enabled | autosave_enabled | bool | True | services/autosave_service.py |
autosave/interval_minutes | autosave_interval_minutes | int | 5 (clamped 1–30 on both read and write) | services/autosave_service.py |
recent_files | recent_files (+ add_recent_file, max 10) | list[str] | [] | File menu in application.py |
startup/show_welcome | show_welcome_on_startup | bool | True | welcome_dialog.py, startup sequence |
updates/skipped_version | skipped_version | str | "" | update checker in application.py ("Skip this version") |
window/geometry and window/state (AppSettings.window_geometry /
window_state) were removed in #285: a never-called duplicate of what
UiStateStore owns under UiState/ (§2). Old real stores may still hold the
orphaned keys; nothing reads them.
Appearance & language
| Key | Property | Type | Default | Read by |
|---|
appearance/theme_mode | theme_mode | ThemeMode (light/dark/system; invalid → SYSTEM) | "system" | ui/theme.py + every color consumer — see trap below |
appearance/language | language | str | "en" | core/i18n.py |
appearance/show_shadows | show_shadows | bool | True | canvas / View menu |
appearance/show_scale_bar | show_scale_bar | bool | True | canvas / View menu |
appearance/show_labels | show_labels | bool | True | canvas / View menu |
appearance/show_constraints | show_constraints | bool | True | canvas / View menu |
appearance/show_spacing_circles | show_spacing_circles | bool | True | canvas_view.py, spacing-ring paint gate |
TRAP (§11.4, verified in docs/11-risks-and-technical-debt/README.md):
ThemeColors.get_colors(ThemeMode.SYSTEM) ignores the user's theme choice.
apply_theme() only writes a stylesheet and never mutates
QApplication.palette(), so SYSTEM-mode detection probes the OS palette. A
user who explicitly picked LIGHT on a dark-default OS gets the dark palette
dict. Rule: only theme.py may call get_colors(ThemeMode.SYSTEM); every
other consumer must call ThemeColors.get_colors(get_settings().theme_mode).
Canvas snap & input (Phase 13 CAD packages)
| Key | Property | Default | Status / note |
|---|
canvas/object_snap_enabled | object_snap_enabled | True | production |
canvas/midpoint_snap_enabled | midpoint_snap_enabled | True | production (US-A3) |
canvas/intersection_snap_enabled | intersection_snap_enabled | True | production (US-A3) |
canvas/nearest_snap_enabled | nearest_snap_enabled | False | production, deliberately default-OFF (US-B4: fallback snap would surprise users relying on free placement near edges) |
canvas/perpendicular_snap_enabled | perpendicular_snap_enabled | False | production, default-OFF opt-in precision aid (US-B5) |
canvas/tangent_snap_enabled | tangent_snap_enabled | False | production, default-OFF opt-in precision aid (US-B6) |
canvas/dynamic_input_enabled | dynamic_input_enabled | True | production (US-A4 cursor overlay + typed coordinates) |
All read by ui/canvas/canvas_view.py / the snap pipeline; toggled from the
View menu. If a snap "doesn't work", check nearest/perpendicular/tangent first —
they default OFF.
Tool memory ("last used" values)
| Key | Property | Default | Read by |
|---|
tools/fillet_last_radius_cm | fillet_last_radius_cm | 25.0 | core/tools/fillet_tool.py (prefills the dialog, US-B3) |
tools/chamfer_last_distance_cm | chamfer_last_distance_cm | 25.0 | same |
Weather & tasks
| Key | Property | Default | Read by |
|---|
weather/frost_warning_orange_c | frost_warning_orange_c | 5.0 °C | frost alerting (US-12.2) |
weather/frost_warning_red_c | frost_warning_red_c | 2.0 °C | frost alerting |
tasks/notify_overdue_on_startup | notify_overdue_tasks_on_startup | True | startup TaskReminderBar (US-C2) |
Agent API (embedded MCP server, US-D1.x)
| Key | Property | Default | Note |
|---|
agent_api/enabled | agent_api_enabled | True | Default ON is a locked product decision (tests/unit/test_agent_api_settings.py asserts the class constants). Read-only tools, loopback-only. Comment in settings.py: token auth must land before D2 write tools default-expose mutate access. |
agent_api/port | agent_api_port | 8765 | Clamped 1024–65535 on read and write. Server URL is http://127.0.0.1:<port>/mcp. |
Startup path (verified in application.py): _setup_agent_api() →
QTimer.singleShot(1500, self._maybe_start_agent_api) → starts only
if get_settings().agent_api_enabled. Preferences dialog restarts/stops the
server when either value changes (compares before/after tuple around the
dialog, ~line 3758).
Plant-DB API keys (stored in QSettings, NOT .env)
| Key | Property | Default |
|---|
api_keys/trefle_token | trefle_api_token | "" |
api_keys/perenual_key | perenual_api_key | "" |
api_keys/permapeople_key_id | permapeople_key_id | "" |
api_keys/permapeople_key_secret | permapeople_key_secret | "" |
Entered in Preferences (with per-API "Test" buttons); consumed by the online
plant-search services. Contrast: the Google Maps key is env-only (§3) because
it must never ship in a binary.
2. UiStateStore — what UI state persists (and what deliberately does not)
Source: src/open_garden_planner/app/ui_state.py. Same QSettings backend as §1
— both take it from app/settings.create_qsettings(), the only store factory in
src/ (#285, ADR-041; gated by tests/unit/test_settings_chokepoint.py, so do
not hand-roll a second one) — with keys under the UiState/ group to avoid
colliding with §1. All production.
| Key | Saved / restored |
|---|
UiState/geometry | QMainWindow.saveGeometry() |
UiState/window_state | QMainWindow.saveState() (docks/toolbars) |
UiState/splitter_<name> | per-named QSplitter.saveState() |
Deliberately NOT persisted (ADR-030 / issue #226): per-panel
collapse/expand/pin state. The sidebar accordion starts fully collapsed every
session; the old save_panel_state/restore_panel_state helpers were
removed (the module docstring says so explicitly). Do not re-add panel
persistence without revisiting ADR-030.
3. Environment variables & .env
| Variable | Status | Purpose | What breaks without it |
|---|
OGP_GOOGLE_MAPS_KEY | production, user-supplied, never bundled | Google Maps satellite background picker (ADR-019, docs/08 §8.15) | The "load satellite background" menu action is disabled with a tooltip telling you to set it (application.py ~line 379); GoogleMapsKeyMissingError if reached anyway. Deliberately env-only: bundling the key in the installer would let it be reverse-engineered and abused (services/google_maps_service.py docstring). |
QT_QPA_PLATFORM=offscreen | production (test infra) | Headless Qt rendering | Set by tests/conftest.py via os.environ.setdefault(...) (line 11) AND by ci.yml's test job env. Without it, tests need a display server. |
PYTHONUTF8=1 | production (script infra) | Forces UTF-8 I/O for scripts/fill_translations.py / compile_translations.py (German umlauts) | Mojibake / encode errors on Windows. Per CLAUDE.md, always run translation scripts with it. |
.env loading (verified in main.py lines 14–27): python-dotenv's
load_dotenv() on the first existing candidate of, in order:
- frozen (PyInstaller):
.env next to the .exe (sys.executable's dir —
the source-relative lookup misses because __file__ sits in _internal/);
- dev: repo-root
.env (three parents up from main.py).
First hit wins; loading happens at module import, before QApplication.
4. Project-file (.ogp) configuration axes
Source: src/open_garden_planner/core/project.py.
FILE_VERSION
- Current:
FILE_VERSION = "1.4" (line 34). 1.4 = Phase 13 Package B's
bezier and arc item types; v1.3 files load transparently.
- What bumps it: only changes an older app could misread — new item
types, changed geometry semantics. What does NOT bump it: additive
top-level keys old builds ignore (
harvest_logs, task_states,
manual_tasks, garden_journal_notes, container/smart-symbol metadata,
…). This additive-key convention is used by every Package C feature.
paper_layouts (deprecated): written only by short-lived US-B7 draft
builds; from_dict intentionally never consumes it (comment at line ~210),
so those files still open.
- Forward-compat rejection (verified,
ProjectManager.load ~line 962):
_is_newer_file_version(file_version, FILE_VERSION) parses X.Y; strictly
newer files raise ValueError ("created by a newer version… please update")
instead of silently dropping unknown content on the next save. Unparseable
or missing version strings are treated as old (returns False), so legacy
files (e.g. "1.0" or no key) still load.
Per-plan settings (live in the file, not QSettings)
ProjectData fields beyond geometry (canvas_width/height, objects,
layers, constraints, guides): location (geo metadata incl. the
satellite-picker scale/coords), season_year + linked_seasons,
task_completions (write-only compat mirror since #230 — read task_states),
seed_inventory, propagation_overrides, crop_rotation, soil_tests,
pest_disease_logs, shopping_list_prices, excluded_shopping_items,
enabled_amendments (None = "all bundled amendments enabled" default) +
prefer_organic (default True), succession_plans,
garden_journal_notes, manual_tasks, task_states, harvest_logs.
Rule of thumb: user preference about the app → §1 QSettings; data or
preference about this garden → ProjectData.
5. Feature-visibility toggles in code (the "experimental flag" pattern)
This repo has no feature-flag framework; experiments ship dark via one-line
visibility calls.
| Toggle | Where | Status |
|---|
| Smart Symbols sidebar panel hidden | application.py line ~1574: self._sidebar_controller.set_panel_visible("smart_symbols", False) | experimental / deferred UI (US-C4). Engine, persistence, DXF export, and properties editing all ship and are tested; symbols in existing .ogp files still regenerate. Re-enable by deleting that one line (the comment says exactly this). |
| Contextual panels hidden until relevant selection | set_panel_visible(key, relevant) for plant_details / companion / crop_rotation in the selection updaters (ADR-030) | production behavior — not a bug when they're absent with nothing selected |
| Satellite menu action disabled | application.py ~line 379 when OGP_GOOGLE_MAPS_KEY unset (§3) | production guard |
| Nearest / perpendicular / tangent snap default-OFF | §1 snap table | production, opt-in |
If "a feature seems disabled", check this table before debugging.
6. Tooling configuration
pyproject.toml (verified in full)
| Section | Contents |
|---|
[tool.ruff] | target-version = "py311", line-length = 100, src = ["src", "tests"] |
[tool.ruff.lint] | select E, W, F, I, B, C4, UP, ARG, SIM; ignore E501, B008 |
| per-file ignores | tests/**/*.py → ARG001, ARG002 (unused-but-required qtbot fixtures) |
[tool.mypy] | strict = true (+ the usual disallow/warn set), follow_imports = "silent", ignore_missing_imports = true; tests.* override relaxes disallow_untyped_defs |
[tool.pytest.ini_options] | testpaths = ["tests"], addopts = "-v --tb=short", qt_api = "pyqt6" |
[tool.coverage] | source = src/open_garden_planner, branch = true, standard exclude lines |
Dependency pins worth knowing (pyproject dependencies)
| Pin | Rationale (from the in-file comment, verified) |
|---|
PyQt6-WebEngine>=6.10.0,<6.11 | upper-bound pin (needed by the map picker's QWebEngineView; must be imported before QApplication — main.py) |
mcp>=1.12,<2.0 | "Pinned to mcp v1 — v2 renames FastMCP -> MCPServer." uvicorn/starlette/pydantic/anyio/sse-starlette come transitively but are listed where used. |
uvicorn>=0.30, starlette>=0.37, pydantic>=2.11 | Agent API server stack |
Deprecated: the top-level requirements.txt lists only PyQt6 + Pillow
— it is stale; pyproject.toml is the dependency source of truth.
installer/ogp.spec knobs (verified; see docs/07 + §8.19 for narrative)
_safe_collect_submodules over concrete MCP subpackages
(mcp.server/mcp.shared/mcp.types, uvicorn, starlette, anyio,
sse_starlette, pydantic, pydantic_settings, httpx_sse) — never top-level
mcp: walking it imports mcp.cli, which sys.exit(1)s without the
unbundled typer extra and aborts the build.
copy_metadata for the same stack (they read distribution metadata at
import time in the frozen exe).
excludes = tkinter/unittest/pydoc/doctest — with an explicit NOTE: do NOT
exclude multiprocessing (uvicorn imports it eagerly; excluding it breaks
the embedded Agent API).
upx_exclude = ["pydantic_core*.pyd"] — UPX can corrupt that compiled
extension.
console=False; data trees copied: icons, textures, plants, objects,
translations, data, web (map-picker HTML).
7. CI axes
ci.yml (verified)
Three jobs on every branch push + PRs to master, all Python 3.11,
ubuntu-latest: lint (ruff check src/), test (apt Qt libs, then
pytest tests/ -v with env QT_QPA_PLATFORM: offscreen), security
(bandit -r src/ --severity-level high). No other env vars or secrets.
release.yml (verified) — details in ogp-change-control
- Triggers on push to master; chore-skip predicate:
!startsWith(message, 'chore:') && !startsWith(message, 'chore(') — the
scoped form was added after chore(finalize-us): slipped through and
wrongly cut a release.
- Version bump from the labels of the most recently merged master PR:
label
major → X+1.0.0, minor → Y+1, else patch (default). Skips
cleanly if the computed tag already exists.
- Builds on windows-latest via
installer/build_installer.py, NSIS installer +
SHA256SUMS, gh release create --generate-notes. Never tag manually.
8. How to add a setting — checklist
Derived from agent_api_enabled (US-D1.1), whose every step verifiably exists;
copy that pattern.
settings.py: add KEY_<NAME> = "group/key_name" constant + a
DEFAULT_<NAME> class constant (+ MIN_/MAX_ if clamped) + a typed
@property/setter pair reading self._settings.value(KEY, DEFAULT, type=...). Clamp in both getter and setter if bounded (see
agent_api_port, autosave_interval_minutes).
- Preferences UI (
ui/dialogs/preferences_dialog.py): add the widget in
the right group box; load current value in the populate block (~line
270), save in _save_and_accept() (ending with settings.sync()).
Reference class constants for ranges (AppSettings.MIN_AGENT_API_PORT).
- i18n: wrap every label/tooltip in
self.tr(...) — no exceptions
(CLAUDE.md MUST).
- Register the strings in
scripts/fill_translations.py (the Agent API
entries sit at ~lines 1875 and 2300), then run
PYTHONUTF8=1 venv/Scripts/python.exe scripts/fill_translations.py and
compile_translations.py;
pytest tests/unit/test_i18n.py::TestTranslationFiles::test_german_ts_has_no_unfinished
must pass.
- Wire the consumer: react to the changed value where it takes effect. If
it needs apply-on-OK, compare before/after around the dialog like
application.py does for the Agent API (~line 3758) and restart/refresh
the affected service.
- Tests: lock the default with a unit test if it's a product decision
(
tests/unit/test_agent_api_settings.py asserts the class constants —
note its rationale: autouse fixtures may override the runtime value, so
assert the constant); add/extend an integration test for the behavior
(tests/integration/test_agent_api_default_on.py).
- Docs: FR entry if user-facing (
docs/functional-requirements.md), ADR
if it's a decision, §11.4 if you hit a pitfall — and update this skill's
§1 table.
- Follow
ogp-change-control for branch/review/draft-PR gates.
Deciding where it lives: user-scoped app behavior → QSettings (this checklist);
plan-scoped → ProjectData field + to_dict/from_dict (additive key, no
FILE_VERSION bump — §4); pure UI geometry → UiStateStore (§2, but never panel
open-state, ADR-030).
Provenance and maintenance
Re-verify each section before relying on it after significant time/version drift:
| Section | Command |
|---|
| §1 keys & defaults | grep -n "KEY_|DEFAULT_|MIN_|MAX_" src/open_garden_planner/app/settings.py |
| §1 QSettings location | grep -n "QSettings(" src/open_garden_planner/app/settings.py src/open_garden_planner/app/ui_state.py |
| §1 theme trap | grep -n "get_colors(ThemeMode.SYSTEM)" -r src/ docs/11-risks-and-technical-debt/ |
| §2 UiState keys | grep -n "UiState|setValue|save_panel" src/open_garden_planner/app/ui_state.py |
| §3 env loading | sed -n '14,28p' src/open_garden_planner/main.py; grep -rn "OGP_GOOGLE_MAPS_KEY|QT_QPA_PLATFORM" src/ tests/conftest.py .github/workflows/ci.yml |
| §4 FILE_VERSION | grep -n "FILE_VERSION|_is_newer_file_version|paper_layouts" src/open_garden_planner/core/project.py |
| §5 hidden panel | grep -n 'set_panel_visible("smart_symbols"' src/open_garden_planner/app/application.py |
| §6 tooling | grep -n "tool.ruff|tool.mypy|tool.pytest|mcp>=|WebEngine" pyproject.toml; grep -n "multiprocessing|mcp.cli|upx_exclude" installer/ogp.spec |
| §7 CI | grep -n "chore|major|minor|QT_QPA_PLATFORM" .github/workflows/release.yml .github/workflows/ci.yml |
| §8 exemplar | grep -n "agent_api" src/open_garden_planner/ui/dialogs/preferences_dialog.py src/open_garden_planner/app/application.py |