From c8fdc21c298daff7cb2deafa71d2c7d6ecd6aa8e Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Thu, 16 Apr 2026 09:16:33 -0700 Subject: [PATCH] feat(export): JSON full-backup download (pantry + saved recipes) Adds GET /export/json that bundles inventory and saved recipes into a single timestamped JSON file for data portability. The export envelope includes schema version and export timestamp so future import logic can handle version differences. Frontend: new primary-styled JSON download button in the Export card with a short description of what is included. Closes #62 --- app/api/endpoints/export.py | 34 ++++++++++++++++++++++- frontend/src/components/InventoryList.vue | 11 +++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/app/api/endpoints/export.py b/app/api/endpoints/export.py index 3c5849c..1bfe8c2 100644 --- a/app/api/endpoints/export.py +++ b/app/api/endpoints/export.py @@ -1,9 +1,11 @@ -"""Export endpoints — CSV/Excel of receipt and inventory data.""" +"""Export endpoints — CSV and JSON export of user data.""" from __future__ import annotations import asyncio import csv import io +import json +from datetime import datetime, timezone from fastapi import APIRouter, Depends from fastapi.responses import StreamingResponse @@ -45,3 +47,33 @@ async def export_inventory_csv(store: Store = Depends(get_store)): media_type="text/csv", headers={"Content-Disposition": "attachment; filename=inventory.csv"}, ) + + +@router.get("/json") +async def export_full_json(store: Store = Depends(get_store)): + """Export full pantry inventory + saved recipes as a single JSON file. + + Intended for data portability — users can import this into another + Kiwi instance or keep it as an offline backup. + """ + inventory, saved = await asyncio.gather( + asyncio.to_thread(store.list_inventory), + asyncio.to_thread(store.get_saved_recipes, 1000, 0), + ) + + export_doc = { + "kiwi_export": { + "version": "1.0", + "exported_at": datetime.now(timezone.utc).isoformat(), + "inventory": [dict(row) for row in inventory], + "saved_recipes": [dict(row) for row in saved], + } + } + + body = json.dumps(export_doc, default=str, indent=2) + filename = f"kiwi-export-{datetime.now(timezone.utc).strftime('%Y%m%d')}.json" + return StreamingResponse( + iter([body]), + media_type="application/json", + headers={"Content-Disposition": f"attachment; filename={filename}"}, + ) diff --git a/frontend/src/components/InventoryList.vue b/frontend/src/components/InventoryList.vue index 1e73c18..b59fc96 100644 --- a/frontend/src/components/InventoryList.vue +++ b/frontend/src/components/InventoryList.vue @@ -392,10 +392,14 @@

Export

-
+
+
+

+ JSON includes pantry + saved recipes. Import it into another Kiwi instance any time. +

@@ -847,6 +851,11 @@ function exportExcel() { window.open(`${apiUrl}/export/inventory/excel`, '_blank') } +function exportJSON() { + const apiUrl = import.meta.env.VITE_API_URL || '/api/v1' + window.open(`${apiUrl}/export/json`, '_blank') +} + // Full date string for tooltip (accessible label) function formatDateFull(dateStr: string): string { const date = new Date(dateStr)