feat(settings): persist storage provider settings to DB, add export endpoint, enrich setup wizard

- dropbox/google_drive/onedrive save-settings: switch to DB as primary,
  .env write as best-effort (no longer fails when .env is absent)
- onedrive/google_drive update-settings: persist changes to DB alongside
  in-memory update; call notify_settings_updated()
- onedrive test_onedrive_token: persist rotated refresh token to DB
- settings_service: add get_settings_for_export() (db / effective modes)
- settings API: add GET /api/settings/export-env (admin-only, downloads .env)
- wizard: enrich settings with current values (DB > ENV > default) and
  value_source badges; pass setup_skipped to template; call
  notify_settings_updated() on save; add /setup/undo-skip route
- setup_wizard.html: pre-populate inputs with current_value; show
  DB/ENV/DEFAULT source badges; skip/undo-skip messaging
- settings.html: replace single Audit Log button with Setup Wizard link,
  Export .env dropdown, and Audit Log button group

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-23 02:41:04 +00:00
parent 0d5c20f685
commit ecc8100e24
8 changed files with 651 additions and 180 deletions
+66 -4
View File
@@ -10,6 +10,7 @@ from fastapi.responses import RedirectResponse
from sqlalchemy.orm import Session
from app.utils.settings_service import save_setting_to_db
from app.utils.settings_sync import notify_settings_updated
from app.utils.setup_wizard import get_wizard_steps
from app.views.base import APIRouter, get_db, templates
@@ -18,7 +19,7 @@ router = APIRouter()
@router.get("/setup")
async def setup_wizard(request: Request, step: int = 1):
async def setup_wizard(request: Request, step: int = 1, db: Session = Depends(get_db)):
"""
Setup wizard for first-time configuration.
@@ -39,7 +40,38 @@ async def setup_wizard(request: Request, step: int = 1):
current_settings = wizard_steps.get(step, [])
# Get step category (all settings in a step should have same category)
step_category = current_settings[0].get("wizard_category", "Configuration") if current_settings else "Configuration"
step_category = (
current_settings[0].get("wizard_category", "Configuration")
if current_settings
else "Configuration"
)
# Enrich settings with current live values
from app.config import settings as app_settings
from app.utils.settings_service import get_setting_from_db
enriched_settings = []
for s in current_settings:
key = s["key"]
db_val = get_setting_from_db(db, key)
env_val = getattr(app_settings, key, None)
# Determine current_value and source
if db_val is not None:
current_value = db_val
value_source = "db"
elif env_val is not None and str(env_val).strip():
current_value = str(env_val)
value_source = "env"
elif s.get("default") is not None:
current_value = s["default"]
value_source = "default"
else:
current_value = ""
value_source = "none"
enriched_settings.append(
{**s, "current_value": current_value, "value_source": value_source}
)
current_settings = enriched_settings
return templates.TemplateResponse(
"setup_wizard.html",
@@ -50,12 +82,15 @@ async def setup_wizard(request: Request, step: int = 1):
"settings": current_settings,
"step_category": step_category,
"progress_percent": int((step / max_step) * 100),
"setup_skipped": bool(get_setting_from_db(db, "_setup_wizard_skipped")),
},
)
@router.post("/setup")
async def setup_wizard_save(request: Request, step: int = Form(...), db: Session = Depends(get_db)):
async def setup_wizard_save(
request: Request, step: int = Form(...), db: Session = Depends(get_db)
):
"""
Save settings from the current wizard step.
"""
@@ -87,6 +122,9 @@ async def setup_wizard_save(request: Request, step: int = Form(...), db: Session
logger.info(f"Setup wizard step {step}: Saved {saved_count} settings")
if saved_count > 0:
notify_settings_updated()
# Determine next step
max_step = max(wizard_steps.keys())
next_step = step + 1
@@ -100,7 +138,9 @@ async def setup_wizard_save(request: Request, step: int = Form(...), db: Session
except Exception as e:
logger.error(f"Error saving wizard settings: {e}")
return RedirectResponse(url=f"/setup?step={step}&error=save_failed", status_code=303)
return RedirectResponse(
url=f"/setup?step={step}&error=save_failed", status_code=303
)
@router.get("/setup/skip")
@@ -122,3 +162,25 @@ async def setup_wizard_skip(request: Request):
except Exception as e:
logger.error(f"Error skipping setup wizard: {e}")
return RedirectResponse(url="/", status_code=303)
@router.get("/setup/undo-skip")
async def setup_wizard_undo_skip(request: Request, db: Session = Depends(get_db)):
"""
Undo a previously skipped setup wizard.
Removes the skip marker from the database so the wizard will be
presented again on next visit to the home page. Redirects to
step 1 of the wizard immediately.
"""
try:
from app.utils.settings_service import delete_setting_from_db
delete_setting_from_db(
db, "_setup_wizard_skipped", changed_by="wizard_undo_skip"
)
logger.info("Setup wizard skip marker removed; redirecting to wizard")
return RedirectResponse(url="/setup?step=1", status_code=303)
except Exception as e:
logger.error(f"Error undoing setup wizard skip: {e}")
return RedirectResponse(url="/settings", status_code=303)