Every fail-open config reader turned a read error on an intact file into
a stand-in ({} from read_raw_config and the TUI's _load_cfg_raw, defaults
or a possibly stale last-known-good from load_config). Writers then
mutated that stand-in and saved it; the writer re-reads the file, finds it
fine, and merges by deletion, so one EMFILE/EIO during a TUI config.set,
a dashboard save, a migration step or any load_config()->save_config()
caller replaced the whole file with the stand-in plus one key.
- Fallbacks from a failed read are now FailedConfigRead (a dict subclass
carrying the error). Readers are unchanged; save_config() and
atomic_config_write() refuse to persist one, so the error reaches the
caller and the file stays byte-identical. The subclass rides the
load->mutate->save round trip, so every writer is covered without
touching each of them.
- A read error's fallback is no longer cached (nor recorded as the next
last-known-good), so the retry reads the file again.
- PUT /api/config merges into a new dict, so it reads strictly instead.
- Gateway /verbose and /footer wrote the effective view back (fail-open
to {} on error, ${VAR} values expanded); they now round-trip the raw
file strictly.