feat: offer guided repair when loading a broken config file

If a config file on disk fails strict JSON parsing, BCC now runs it
through the same repair pipeline as pasted snippets and shows a dialog
listing the parse error, each fix it would apply, and a preview of the
resulting file. The user chooses: Repair & load (marks the profile
dirty; the file is only rewritten on Save, after the broken original
is backed up) or Cancel. Unsalvageable files keep the old error path.

repair_config_file() in bcc_core never writes to disk itself.
This commit is contained in:
AJ
2026-07-02 00:10:27 -04:00
parent d6ce4a0fb0
commit 77f469c1dd
3 changed files with 132 additions and 6 deletions
+74 -6
View File
@@ -632,6 +632,56 @@ class ArgsEdit(QPlainTextEdit):
n += 1
# --------------------------------------------------------------------------- #
# Config-repair dialog: shown when a config file on disk fails strict JSON
# parsing but the repair pipeline can salvage it. Lists what's wrong, what
# would change, and lets the user decide. Nothing touches disk until Save.
# --------------------------------------------------------------------------- #
class RepairDialog(QDialog):
def __init__(self, parent, path, error: str, notes: list[str], preview: str):
super().__init__(parent)
self.setWindowTitle("Config file needs repair")
self.resize(620, 520)
v = QVBoxLayout(self)
v.setSpacing(10)
intro = QLabel(f"<b>{path}</b><br>isn't valid JSON: {error}")
intro.setWordWrap(True)
v.addWidget(intro)
fixes_hdr = QLabel("BCC can repair it automatically. Fixes to apply:")
v.addWidget(fixes_hdr)
fixes = QLabel("\n".join(f"{n}" for n in notes) or " • reformat as valid JSON")
fixes.setObjectName("muted")
fixes.setWordWrap(True)
v.addWidget(fixes)
prev_hdr = QLabel("The file would become:")
v.addWidget(prev_hdr)
box = QPlainTextEdit()
box.setObjectName("diag")
box.setReadOnly(True)
box.setPlainText(preview)
v.addWidget(box, 1)
note = QLabel(
"Nothing is written yet — choosing Repair loads the fixed config into the "
"editor. The file is only rewritten when you press Save, and the broken "
"original is backed up first."
)
note.setObjectName("muted")
note.setWordWrap(True)
v.addWidget(note)
btns = QDialogButtonBox()
ok = btns.addButton("Repair && load", QDialogButtonBox.ButtonRole.AcceptRole)
ok.setObjectName("primary")
btns.addButton(QDialogButtonBox.StandardButton.Cancel)
btns.accepted.connect(self.accept)
btns.rejected.connect(self.reject)
v.addWidget(btns)
# --------------------------------------------------------------------------- #
# Paste-JSON dialog
# --------------------------------------------------------------------------- #
@@ -972,15 +1022,27 @@ class MainWindow(QMainWindow):
return
if confirm and self.dirty and not self._confirm_discard():
return
repaired = False
try:
self.full_config = core.load_config(profile.path)
except Exception as e:
QMessageBox.critical(
self,
"Could not read config",
f"{profile.path}\n\n{e}\n\nFix the file by hand or pick another install.",
)
return
# Strict parse failed — see if the repair pipeline can salvage it,
# and let the user decide with full visibility of the changes.
try:
cfg, notes, preview = core.repair_config_file(profile.path)
except Exception:
QMessageBox.critical(
self,
"Could not read config",
f"{profile.path}\n\n{e}\n\nThe file couldn't be repaired automatically. "
f"Fix it by hand or pick another install.",
)
return
dlg = RepairDialog(self, profile.path, str(e), notes, preview)
if not dlg.exec():
return
self.full_config = cfg
repaired = True
self.current_profile = profile
self.servers = core.extract_servers(self.full_config)
self.dirty = False
@@ -988,6 +1050,12 @@ class MainWindow(QMainWindow):
self.undo_btn.setEnabled(False)
self._refresh_tables(select_index=0 if self.servers else -1)
self._update_status(saved=False)
if repaired:
self._mark_dirty()
self.status.setText(
f"{profile.path} · repaired config loaded — press Save to write the "
f"fix (the original file will be backed up)"
)
# --- table rendering ------------------------------------------------- #
def _add_row(self, table, master_idx, s):