fix+ux(settings): clearer provider/model flow, inline provider editing

- Merge Anbieter and Standard-LLM-Parameter into one card with explicit
  step labels (Schritt 1: Anbieter, Schritt 2: Modell setzen)
- Show current default provider name in step 2 so it's always visible
- Add inline edit form per provider row (pencil button toggles form)
- Add /providers/{id}/update and /providers/{id}/clear-key routes
- Fix sqlite3.Row.get() AttributeError in _prompt_llm_block and
  _process_analysis_job (use .keys() instead)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 14:52:19 +02:00
parent b26fff0d0f
commit a2ddb6b94d

112
app.py
View File

@@ -545,7 +545,7 @@ def _process_analysis_job(job_id: int):
repeat_penalty = float(get_setting("llm_repeat_penalty", "1.15")) repeat_penalty = float(get_setting("llm_repeat_penalty", "1.15"))
repeat_last_n = int(get_setting("llm_repeat_last_n", "128")) repeat_last_n = int(get_setting("llm_repeat_last_n", "128"))
else: else:
provider = _get_provider(prm.get("provider_id")) provider = _get_provider(prm["provider_id"] if "provider_id" in prm.keys() else None)
model = prm["llm_model"] or get_setting("llm_model", _SEED_MODEL) model = prm["llm_model"] or get_setting("llm_model", _SEED_MODEL)
num_ctx_setting = prm["llm_num_ctx"] or get_setting("llm_num_ctx", "auto") num_ctx_setting = prm["llm_num_ctx"] or get_setting("llm_num_ctx", "auto")
num_predict = int(prm["llm_num_predict"] or get_setting("llm_num_predict", str(_SEED_NUM_PREDICT))) num_predict = int(prm["llm_num_predict"] or get_setting("llm_num_predict", str(_SEED_NUM_PREDICT)))
@@ -795,17 +795,46 @@ def settings_page(msg: str = ""):
email_addrs = c.execute("SELECT id, label, email FROM email_addresses ORDER BY label").fetchall() email_addrs = c.execute("SELECT id, label, email FROM email_addresses ORDER BY label").fetchall()
provider_rows = "".join( provider_rows = "".join(
f"""<tr> f"""<tr id='prow_{p["id"]}'>
<td>{p["name"]}</td> <td class='align-middle'><span class='fw-semibold'>{p["name"]}</span></td>
<td><code class='small'>{p["base_url"]}</code></td> <td class='align-middle'><code class='small'>{p["base_url"]}</code></td>
<td class='text-center'><i class='bi bi-{'key-fill text-success' if p["api_key"] else 'key text-secondary'}'></i></td> <td class='text-center align-middle'><i class='bi bi-{"key-fill text-success" if p["api_key"] else "key text-secondary"}' title='{"API-Key gesetzt" if p["api_key"] else "Kein API-Key"}'></i></td>
<td class='text-center'>{'<span class=\'badge bg-primary\'>Standard</span>' if p["is_default"] else f'<form method=\'post\' action=\'/providers/{p["id"]}/set-default\' class=\'d-inline\'><button class=\'btn btn-sm btn-outline-secondary py-0 px-2\'>Standard</button></form>'}</td> <td class='text-center align-middle'>
<td class='text-end'> {"<span class='badge bg-primary'>Standard</span>" if p["is_default"] else f"<form method='post' action='/providers/{p['id']}/set-default' class='d-inline'><button class='btn btn-sm btn-outline-secondary py-0 px-2' title='Als Standard setzen'><i class='bi bi-star'></i></button></form>"}
</td>
<td class='text-end align-middle'>
<button class='btn btn-sm btn-outline-primary py-0 px-2 me-1' type='button'
onclick='toggleEdit({p["id"]})' title='Bearbeiten'><i class='bi bi-pencil'></i></button>
<form method='post' action='/providers/{p["id"]}/delete' class='d-inline' <form method='post' action='/providers/{p["id"]}/delete' class='d-inline'
onsubmit='return confirm("Anbieter löschen?")'> onsubmit='return confirm("Anbieter löschen?")'>
<button class='btn btn-sm btn-outline-danger py-0 px-2'><i class='bi bi-trash'></i></button> <button class='btn btn-sm btn-outline-danger py-0 px-2'><i class='bi bi-trash'></i></button>
</form> </form>
</td> </td>
</tr>
<tr id='pedit_{p["id"]}' class='d-none bg-body-tertiary'>
<td colspan='5' class='p-3'>
<form method='post' action='/providers/{p["id"]}/update'>
<div class='row g-2 align-items-end'>
<div class='col-12 col-md-3'>
<label class='form-label small mb-1'>Name</label>
<input class='form-control form-control-sm' name='name' value='{p["name"]}' required>
</div>
<div class='col-12 col-md-4'>
<label class='form-label small mb-1'>Base-URL</label>
<input class='form-control form-control-sm font-monospace' name='base_url' value='{p["base_url"]}' required>
</div>
<div class='col-12 col-md-3'>
<label class='form-label small mb-1'>API-Key <span class='text-secondary'>(leer lassen = unverändert)</span></label>
<input class='form-control form-control-sm' type='password' name='api_key' placeholder='{"••••••••" if p["api_key"] else "kein Key"}' autocomplete='new-password'>
</div>
<div class='col-auto d-flex gap-2 align-items-end'>
<button class='btn btn-primary btn-sm' type='submit'><i class='bi bi-check2'></i> Speichern</button>
<button class='btn btn-outline-secondary btn-sm' type='button' onclick='toggleEdit({p["id"]})'><i class='bi bi-x'></i></button>
{"<form method='post' action='/providers/" + str(p['id']) + "/clear-key' class='d-inline'><button class='btn btn-outline-warning btn-sm' type='submit' title='API-Key löschen'><i class='bi bi-key-x'></i></button></form>" if p["api_key"] else ""}
</div>
</div>
</form>
</td>
</tr>""" </tr>"""
for p in providers for p in providers
) )
@@ -842,9 +871,14 @@ def settings_page(msg: str = ""):
{f"<div class='alert alert-success py-2'>{msg}</div>" if msg else ""} {f"<div class='alert alert-success py-2'>{msg}</div>" if msg else ""}
<div class='card' id='anbieter'> <div class='card' id='anbieter'>
<h5 class='h6 fw-semibold mb-3'><i class='bi bi-hdd-network text-primary me-1'></i> LLM-Anbieter</h5> <h5 class='h6 fw-semibold mb-3'><i class='bi bi-hdd-network text-primary me-1'></i> LLM-Anbieter &amp; Standard-Modell</h5>
<p class='text-secondary small mb-2'>OpenAI-kompatible Endpunkte (Ollama, OpenAI, Groq, LM Studio, …)</p>
{"<table class='table table-sm mb-3'><thead><tr><th>Name</th><th>Base-URL</th><th class='text-center'>API-Key</th><th class='text-center'>Standard</th><th></th></tr></thead><tbody>" + provider_rows + "</tbody></table>" if providers else "<p class='text-secondary small'>Noch kein Anbieter konfiguriert.</p>"} <h6 class='small text-secondary fw-semibold mb-2'>SCHRITT 1 — Anbieter verwalten</h6>
<p class='text-secondary small mb-2'>OpenAI-kompatible Endpunkte (Ollama, OpenAI, Groq, LM Studio, …). <i class='bi bi-star'></i> = als Standard setzen.</p>
{"<div class='table-responsive'><table class='table table-sm mb-3'><thead><tr><th>Name</th><th>Base-URL</th><th class='text-center'>API-Key</th><th class='text-center'><i class='bi bi-star' title='Standard'></i></th><th></th></tr></thead><tbody>" + provider_rows + "</tbody></table></div>" if providers else "<p class='text-secondary small'>Noch kein Anbieter konfiguriert.</p>"}
<details class='mb-4'>
<summary class='text-secondary small' style='cursor:pointer'><i class='bi bi-plus-circle me-1'></i> Neuen Anbieter hinzufügen</summary>
<div class='mt-2'>
<form method='post' action='/providers/add' class='row g-2 align-items-end'> <form method='post' action='/providers/add' class='row g-2 align-items-end'>
<div class='col-12 col-md-3'> <div class='col-12 col-md-3'>
<label class='form-label'>Name</label> <label class='form-label'>Name</label>
@@ -861,23 +895,28 @@ def settings_page(msg: str = ""):
<div class='col-auto d-flex align-items-end gap-2'> <div class='col-auto d-flex align-items-end gap-2'>
<div class='form-check mb-2'> <div class='form-check mb-2'>
<input class='form-check-input' type='checkbox' name='is_default' value='1' id='provDefault'> <input class='form-check-input' type='checkbox' name='is_default' value='1' id='provDefault'>
<label class='form-check-label' for='provDefault'>Standard</label> <label class='form-check-label' for='provDefault'>Als Standard</label>
</div> </div>
<button class='btn btn-outline-primary mb-0' type='submit'><i class='bi bi-plus-lg'></i> Hinzufügen</button> <button class='btn btn-outline-primary mb-0' type='submit'><i class='bi bi-plus-lg'></i> Hinzufügen</button>
</div> </div>
</form> </form>
</div> </div>
</details>
<div class='card mt-3'> <hr class='my-3'>
<h5 class='h6 fw-semibold mb-3'><i class='bi bi-sliders text-primary me-1'></i> Standard-LLM-Parameter</h5> <h6 class='small text-secondary fw-semibold mb-2'>SCHRITT 2 — Standard-Modell &amp; Parameter festlegen</h6>
<p class='text-secondary small mb-2'>Gelten für alle Prompts ohne eigene Einstellungen. Anbieter = markierter Standard-Anbieter oben.</p> <p class='text-secondary small mb-3'>
Gilt für alle Prompts ohne eigene Einstellungen. Standard-Anbieter:
<strong>{next((p["name"] for p in providers if p["is_default"]), " keiner gesetzt ")}</strong>
</p>
<form method='post' action='/settings'> <form method='post' action='/settings'>
<div class='row g-3'> <div class='row g-3'>
<div class='col-12'> <div class='col-12'>
<label class='form-label'>Standard-Modell</label> <label class='form-label'>Standard-Modell</label>
<div class='input-group'> <div class='input-group'>
<select id='providerSelect' class='form-select' style='max-width:220px' onchange='updateProviderForLoad()'> <select id='providerSelect' class='form-select flex-grow-0' style='width:auto;max-width:200px'
title='Anbieter für Modellliste wählen' onchange='onProviderChange()'>
{provider_id_opts} {provider_id_opts}
</select> </select>
<input id='modelInput' class='form-control font-monospace' name='llm_model' <input id='modelInput' class='form-control font-monospace' name='llm_model'
@@ -889,7 +928,7 @@ def settings_page(msg: str = ""):
<div id='modelSelectWrap' class='mt-1 d-none'> <div id='modelSelectWrap' class='mt-1 d-none'>
<select id='modelSelect' class='form-select font-monospace'></select> <select id='modelSelect' class='form-select font-monospace'></select>
</div> </div>
<div class='form-text'>Aktuell gespeichert: <code>{current_model}</code></div> <div class='form-text'>Anbieter im Dropdown links wählen → <em>Modelle laden</em> → Modell auswählen → Speichern. Aktuell: <code>{current_model}</code></div>
</div> </div>
<div class='col-12 col-md-4'> <div class='col-12 col-md-4'>
@@ -990,21 +1029,24 @@ def settings_page(msg: str = ""):
</div> </div>
<script> <script>
let _loadProviderId = null; function onProviderChange() {{
function updateProviderForLoad() {{
_loadProviderId = document.getElementById('providerSelect')?.value || null;
document.getElementById('modelSelectWrap').classList.add('d-none'); document.getElementById('modelSelectWrap').classList.add('d-none');
}} }}
function toggleEdit(pid) {{
const row = document.getElementById('pedit_' + pid);
row.classList.toggle('d-none');
}}
async function loadModels() {{ async function loadModels() {{
const btn = document.getElementById('loadModelsBtn'); const btn = document.getElementById('loadModelsBtn');
const icon = document.getElementById('loadIcon'); const icon = document.getElementById('loadIcon');
const wrap = document.getElementById('modelSelectWrap'); const wrap = document.getElementById('modelSelectWrap');
const sel = document.getElementById('modelSelect'); const sel = document.getElementById('modelSelect');
const inp = document.getElementById('modelInput'); const inp = document.getElementById('modelInput');
const pid = document.getElementById('providerSelect')?.value || '';
btn.disabled = true; btn.disabled = true;
icon.className = 'bi bi-hourglass-split'; icon.className = 'bi bi-hourglass-split';
const pid = _loadProviderId || document.getElementById('providerSelect')?.value || '';
try {{ try {{
const res = await fetch('/settings/models' + (pid ? '?provider_id=' + pid : '')); const res = await fetch('/settings/models' + (pid ? '?provider_id=' + pid : ''));
const data = await res.json(); const data = await res.json();
@@ -1118,6 +1160,34 @@ def set_default_provider(provider_id: int):
return RedirectResponse("/settings?msg=Standard-Anbieter+gesetzt.#anbieter", status_code=303) return RedirectResponse("/settings?msg=Standard-Anbieter+gesetzt.#anbieter", status_code=303)
@app.post("/providers/{provider_id}/update", response_class=HTMLResponse)
def update_provider(
provider_id: int,
name: str = Form(...),
base_url: str = Form(...),
api_key: str = Form(""),
):
with db() as c:
if api_key.strip():
c.execute(
"UPDATE providers SET name=?, base_url=?, api_key=? WHERE id=?",
(name.strip(), base_url.strip().rstrip("/"), api_key.strip(), provider_id),
)
else:
c.execute(
"UPDATE providers SET name=?, base_url=? WHERE id=?",
(name.strip(), base_url.strip().rstrip("/"), provider_id),
)
return RedirectResponse("/settings?msg=Anbieter+aktualisiert.#anbieter", status_code=303)
@app.post("/providers/{provider_id}/clear-key", response_class=HTMLResponse)
def clear_provider_key(provider_id: int):
with db() as c:
c.execute("UPDATE providers SET api_key=NULL WHERE id=?", (provider_id,))
return RedirectResponse("/settings?msg=API-Key+gelöscht.#anbieter", status_code=303)
@app.get("/", response_class=HTMLResponse) @app.get("/", response_class=HTMLResponse)
def upload_page(msg: str = ""): def upload_page(msg: str = ""):
projects = get_projects() projects = get_projects()
@@ -1775,7 +1845,7 @@ def _prompt_llm_block(p, providers, def_model, def_num_ctx, def_num_predict, def
np_ = p["llm_num_predict"] or def_num_predict np_ = p["llm_num_predict"] or def_num_predict
rp = p["llm_repeat_penalty"] or def_repeat_penalty rp = p["llm_repeat_penalty"] or def_repeat_penalty
rn = p["llm_repeat_last_n"] or def_repeat_last_n rn = p["llm_repeat_last_n"] or def_repeat_last_n
cur_provider_id = p.get("provider_id") cur_provider_id = p["provider_id"] if "provider_id" in p.keys() else None
disabled = "disabled" if use_default else "" disabled = "disabled" if use_default else ""
opacity = "opacity:.45;pointer-events:none" if use_default else "" opacity = "opacity:.45;pointer-events:none" if use_default else ""
ctx_opts = [("auto","Automatisch"), ("4096","4 096"), ("8192","8 192"), ("16384","16 384"), ctx_opts = [("auto","Automatisch"), ("4096","4 096"), ("8192","8 192"), ("16384","16 384"),