Skip to content

Commit da864e0

Browse files
committed
feat: surface knowledge and settings counts in web restore banner
- Remove learning-state.md from backup/restore (derived data, not worth persisting as a first-class export artifact) - Pass topics and settings counts through the /settings/import redirect so the result banner confirms knowledge map and settings were restored - settings_page accepts topics + settings query params and forwards them to the template - settings.html banner shows N topics restored and Settings restored
1 parent 250d520 commit da864e0

4 files changed

Lines changed: 25 additions & 14 deletions

File tree

src/devcoach/core/db.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -457,15 +457,13 @@ def create_backup_zip(conn: sqlite3.Connection) -> bytes:
457457
zf.writestr("settings.json", json.dumps(settings.model_dump(), indent=2))
458458
zf.writestr("lessons.json", json.dumps(lessons, indent=2, ensure_ascii=False))
459459
zf.writestr("knowledge.json", json.dumps(knowledge_data, indent=2))
460-
if LEARNING_STATE_PATH.exists():
461-
zf.writestr("learning-state.md", LEARNING_STATE_PATH.read_text(encoding="utf-8"))
462460
return buf.getvalue()
463461

464462

465463
def restore_backup_zip(conn: sqlite3.Connection, data: bytes) -> dict[str, int]:
466464
"""Restore from a backup zip.
467465
468-
Returns a dict with counts: {"settings": 1, "topics": N, "lessons": N, "skipped": N, "invalid": N}.
466+
Returns a dict with counts: {"settings": 1, "topics": N, "groups": N, "lessons": N, "skipped": N, "invalid": N}.
469467
Settings are overwritten; knowledge entries are upserted; duplicate lessons are skipped.
470468
"""
471469
result: dict[str, int] = {
@@ -475,7 +473,6 @@ def restore_backup_zip(conn: sqlite3.Connection, data: bytes) -> dict[str, int]:
475473
"lessons": 0,
476474
"skipped": 0,
477475
"invalid": 0,
478-
"learning_state": 0,
479476
}
480477

481478
with zipfile.ZipFile(io.BytesIO(data)) as zf:
@@ -521,13 +518,6 @@ def restore_backup_zip(conn: sqlite3.Connection, data: bytes) -> dict[str, int]:
521518
result["skipped"] = duplicated
522519
result["invalid"] = invalid
523520

524-
if "learning-state.md" in names:
525-
LEARNING_STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
526-
LEARNING_STATE_PATH.write_text(
527-
zf.read("learning-state.md").decode("utf-8"), encoding="utf-8"
528-
)
529-
result["learning_state"] = 1
530-
531521
return result
532522

533523

src/devcoach/web/app.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import json
66
from pathlib import Path
77

8-
from fastapi import FastAPI, File, Form, Request, UploadFile
8+
from fastapi import FastAPI, File, Form, Query, Request, UploadFile
99
from fastapi.responses import HTMLResponse, RedirectResponse, Response
1010
from fastapi.staticfiles import StaticFiles
1111
from fastapi.templating import Jinja2Templates
@@ -293,6 +293,8 @@ async def settings_page(
293293
skipped: int | None = None,
294294
invalid: int | None = None,
295295
groups: int | None = None,
296+
topics: int | None = None,
297+
settings_restored: int | None = Query(None, alias="settings"),
296298
) -> HTMLResponse:
297299
with db.connection() as conn:
298300
settings = db.get_settings(conn)
@@ -307,6 +309,8 @@ async def settings_page(
307309
"skipped": skipped,
308310
"invalid": invalid,
309311
"groups": groups,
312+
"topics": topics,
313+
"settings_restored": settings_restored,
310314
},
311315
)
312316

@@ -345,6 +349,10 @@ async def import_settings_route(file: UploadFile = File(...)) -> RedirectRespons
345349
with db.connection() as conn:
346350
result = db.restore_backup_zip(conn, content)
347351
return RedirectResponse(
348-
url=f"/settings?imported={result['lessons']}&skipped={result['skipped']}&invalid={result['invalid']}&groups={result['groups']}",
352+
url=(
353+
f"/settings?imported={result['lessons']}&skipped={result['skipped']}"
354+
f"&invalid={result['invalid']}&groups={result['groups']}"
355+
f"&topics={result['topics']}&settings={result['settings']}"
356+
),
349357
status_code=303,
350358
)

src/devcoach/web/templates/settings.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ <h1 class="text-2xl font-bold mb-6 text-indigo-600 dark:text-indigo-400">Setting
1414
{{ imported }} lesson{{ 's' if imported != 1 }} imported.
1515
{%- if skipped %} {{ skipped }} skipped (already in DB).{% endif %}
1616
{%- if invalid %} {{ invalid }} rejected (failed validation).{% endif %}
17-
{%- if groups is not none %} {{ groups }} group{{ 's' if groups != 1 }} added.{% endif %}
17+
{%- if topics %} {{ topics }} topic{{ 's' if topics != 1 }} restored.{% endif %}
18+
{%- if groups %} {{ groups }} group{{ 's' if groups != 1 }} added.{% endif %}
19+
{%- if settings_restored %} Settings restored.{% endif %}
1820
</div>
1921
{% endif %}
2022

tests/test_web_extra.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,17 @@ def test_import_shows_counts(self, client):
157157
)
158158
assert "imported=2" in str(r.url) or "2" in r.text
159159

160+
def test_import_redirect_includes_topics_and_settings(self, client):
161+
r = client.post(
162+
"/settings/import",
163+
files={"file": ("backup.zip", self._make_backup_zip(), "application/zip")},
164+
follow_redirects=False,
165+
)
166+
location = r.headers["location"]
167+
assert "topics=" in location
168+
assert "settings=" in location
169+
170+
160171

161172
# ── GET /lessons/export ────────────────────────────────────────────────────
162173

0 commit comments

Comments
 (0)