Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f2fb81024e | |||
| 76f0e83a65 | |||
| c85a7b78a6 | |||
| 6f9716c32b | |||
| 96f4a5d228 | |||
| 827dfcdf2f |
@@ -1,3 +1,3 @@
|
|||||||
830554
|
833504
|
||||||
830555
|
833505
|
||||||
830569
|
833519
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import yaml
|
import yaml
|
||||||
import logging
|
import logging
|
||||||
from .config_loader import load_config, get_config
|
from .config_loader import load_config as loader_load_config, get_config as loader_get_config
|
||||||
|
|
||||||
log = logging.getLogger("ainventory")
|
log = logging.getLogger("ainventory")
|
||||||
|
|
||||||
@@ -52,8 +52,8 @@ class ConfigManager:
|
|||||||
log.info(f"✅ Updated {path} with new values.")
|
log.info(f"✅ Updated {path} with new values.")
|
||||||
|
|
||||||
# Reload the global config
|
# Reload the global config
|
||||||
load_config()
|
loader_load_config()
|
||||||
return get_config()
|
return loader_get_config()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"❌ Failed to write {path}: {e}")
|
log.error(f"❌ Failed to write {path}: {e}")
|
||||||
raise
|
raise
|
||||||
@@ -103,8 +103,8 @@ class ConfigManager:
|
|||||||
log.info(f"✅ Updated {path} with new secrets.")
|
log.info(f"✅ Updated {path} with new secrets.")
|
||||||
|
|
||||||
# Reload the global config
|
# Reload the global config
|
||||||
load_config()
|
loader_load_config()
|
||||||
return get_config()
|
return loader_get_config()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"❌ Failed to write {path}: {e}")
|
log.error(f"❌ Failed to write {path}: {e}")
|
||||||
raise
|
raise
|
||||||
@@ -133,17 +133,17 @@ class ConfigManager:
|
|||||||
if backend_updates:
|
if backend_updates:
|
||||||
ConfigManager.update_config(backend_updates)
|
ConfigManager.update_config(backend_updates)
|
||||||
|
|
||||||
return get_config()
|
return loader_get_config()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_config() -> dict:
|
def get_config() -> dict:
|
||||||
"""Return the current global configuration."""
|
"""Return the current global configuration."""
|
||||||
return get_config()
|
return loader_get_config()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_masked_key(key_name: str):
|
def get_masked_key(key_name: str):
|
||||||
"""Returns a masked version of a configuration value or env var."""
|
"""Returns a masked version of a configuration value or env var."""
|
||||||
config = get_config()
|
config = loader_get_config()
|
||||||
|
|
||||||
# Try to find in config first
|
# Try to find in config first
|
||||||
val = None
|
val = None
|
||||||
@@ -183,7 +183,7 @@ def update_config(updates: dict) -> dict:
|
|||||||
|
|
||||||
def get_config() -> dict:
|
def get_config() -> dict:
|
||||||
"""Return the current global configuration."""
|
"""Return the current global configuration."""
|
||||||
return ConfigManager.get_config()
|
return loader_get_config()
|
||||||
|
|
||||||
def validate_config_file() -> bool:
|
def validate_config_file() -> bool:
|
||||||
"""Validate backend.yaml syntax and required fields."""
|
"""Validate backend.yaml syntax and required fields."""
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Supports CSV and Excel formats.
|
|||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from backend.database import get_db
|
from backend.database import get_db
|
||||||
@@ -70,20 +70,16 @@ async def export_inventory_snapshot(
|
|||||||
|
|
||||||
# Return file response
|
# Return file response
|
||||||
if format_type == "csv":
|
if format_type == "csv":
|
||||||
# For CSV, use FileResponse with bytes
|
return StreamingResponse(
|
||||||
import io
|
|
||||||
return FileResponse(
|
|
||||||
io.BytesIO(content.encode("utf-8")),
|
io.BytesIO(content.encode("utf-8")),
|
||||||
media_type=media_type,
|
media_type=media_type,
|
||||||
filename=filename,
|
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# For Excel, content is already bytes
|
return StreamingResponse(
|
||||||
import io
|
|
||||||
return FileResponse(
|
|
||||||
io.BytesIO(content),
|
io.BytesIO(content),
|
||||||
media_type=media_type,
|
media_type=media_type,
|
||||||
filename=filename,
|
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -126,22 +122,20 @@ async def export_audit_trail(
|
|||||||
|
|
||||||
# Return file response
|
# Return file response
|
||||||
if format_type == "csv":
|
if format_type == "csv":
|
||||||
import io
|
return StreamingResponse(
|
||||||
return FileResponse(
|
|
||||||
io.BytesIO(content.encode("utf-8")),
|
io.BytesIO(content.encode("utf-8")),
|
||||||
media_type=media_type,
|
media_type=media_type,
|
||||||
filename=filename,
|
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
import io
|
return StreamingResponse(
|
||||||
return FileResponse(
|
|
||||||
io.BytesIO(content),
|
io.BytesIO(content),
|
||||||
media_type=media_type,
|
media_type=media_type,
|
||||||
filename=filename,
|
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/db/export")
|
@router.get("/reports/export")
|
||||||
async def export_db(
|
async def export_db(
|
||||||
format: str = Query("csv", description="Export format: csv or xlsx"),
|
format: str = Query("csv", description="Export format: csv or xlsx"),
|
||||||
type: str = Query("inventory", description="Export type: inventory, audit, or combined"),
|
type: str = Query("inventory", description="Export type: inventory, audit, or combined"),
|
||||||
@@ -226,14 +220,14 @@ async def export_db(
|
|||||||
|
|
||||||
# Return file response
|
# Return file response
|
||||||
if format_type == "csv":
|
if format_type == "csv":
|
||||||
return FileResponse(
|
return StreamingResponse(
|
||||||
io.BytesIO(content.encode("utf-8")),
|
io.BytesIO(content.encode("utf-8")),
|
||||||
media_type=media_type,
|
media_type=media_type,
|
||||||
filename=filename,
|
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return FileResponse(
|
return StreamingResponse(
|
||||||
io.BytesIO(content),
|
io.BytesIO(content),
|
||||||
media_type=media_type,
|
media_type=media_type,
|
||||||
filename=filename,
|
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||||
)
|
)
|
||||||
|
|||||||
BIN
backups/ainventory_2026-04-23_14-45-07.tar.gz
Normal file
BIN
backups/ainventory_2026-04-23_14-45-07.tar.gz
Normal file
Binary file not shown.
BIN
backups/ainventory_2026-04-23_14-51-52.tar.gz
Normal file
BIN
backups/ainventory_2026-04-23_14-51-52.tar.gz
Normal file
Binary file not shown.
BIN
backups/ainventory_2026-04-23_15-08-46.tar.gz
Normal file
BIN
backups/ainventory_2026-04-23_15-08-46.tar.gz
Normal file
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "1.14.12",
|
"version": "1.14.15",
|
||||||
"last_build": "2026-04-23-1445",
|
"last_build": "2026-04-23-1514",
|
||||||
"codename": "ConfigCore",
|
"codename": "ConfigCore",
|
||||||
"commit": "c14a28b0"
|
"commit": "76f0e83a"
|
||||||
}
|
}
|
||||||
@@ -1,136 +1,132 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { FileDown, Loader2 } from "lucide-react";
|
import { FileDown, Loader2, FileSpreadsheet, FileText } from "lucide-react";
|
||||||
import { useExport } from "@/hooks/useExport";
|
import { useExport } from "@/hooks/useExport";
|
||||||
import { Toast } from "@/components/Toast";
|
import { toast } from "react-hot-toast";
|
||||||
|
|
||||||
export function ExportPanel() {
|
export function ExportPanel() {
|
||||||
const { exportSnapshot, exportAuditTrail, isLoading, error } = useExport();
|
const { exportSnapshot, exportAuditTrail, isLoading, error } = useExport();
|
||||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const handleExportSnapshot = async (format: "csv" | "xlsx") => {
|
const handleExportSnapshot = async (format: "csv" | "xlsx") => {
|
||||||
try {
|
try {
|
||||||
setSuccessMessage(null);
|
|
||||||
await exportSnapshot(format);
|
await exportSnapshot(format);
|
||||||
const formatName = format === "csv" ? "CSV" : "Excel";
|
const formatName = format === "csv" ? "CSV" : "Excel";
|
||||||
setSuccessMessage(`Inventory snapshot exported as ${formatName}`);
|
toast.success(`Inventory snapshot exported as ${formatName}`);
|
||||||
setTimeout(() => setSuccessMessage(null), 4000);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Error handled by useExport hook
|
// Error handled by hook
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExportAuditTrail = async (format: "csv" | "xlsx") => {
|
const handleExportAuditTrail = async (format: "csv" | "xlsx") => {
|
||||||
try {
|
try {
|
||||||
setSuccessMessage(null);
|
|
||||||
await exportAuditTrail(format);
|
await exportAuditTrail(format);
|
||||||
const formatName = format === "csv" ? "CSV" : "Excel";
|
const formatName = format === "csv" ? "CSV" : "Excel";
|
||||||
setSuccessMessage(`Audit trail exported as ${formatName}`);
|
toast.success(`Audit trail exported as ${formatName}`);
|
||||||
setTimeout(() => setSuccessMessage(null), 4000);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Error handled by useExport hook
|
// Error handled by hook
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div data-testid="admin-tab-exports" className="p-4 md:p-6 bg-surface border border-slate-800 rounded-3xl space-y-4 md:space-y-6">
|
||||||
<div className="rounded-lg border border-slate-200 bg-white p-6">
|
<header className="flex items-center gap-3">
|
||||||
<div className="mb-6 flex items-center gap-3">
|
<div className="p-3 md:p-4 bg-indigo-500/10 rounded-2xl text-indigo-400 border border-indigo-500/20 shadow-xl shadow-indigo-500/5">
|
||||||
<div className="rounded-lg bg-primary/10 p-4 border border-primary/20">
|
<FileDown size={28} className="md:w-8 md:h-8" />
|
||||||
<FileDown className="text-primary" size={24} />
|
</div>
|
||||||
</div>
|
<div>
|
||||||
|
<h2 className="text-xl md:text-2xl font-normal text-white">Export & Reports</h2>
|
||||||
|
<p className="text-[10px] md:text-xs text-muted font-normal tracking-tight mt-0.5">
|
||||||
|
Download inventory snapshots and audit trails
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
{/* Inventory Snapshot Section */}
|
||||||
|
<div className="p-4 bg-slate-800/40 border border-slate-800 rounded-2xl space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-normal">Export & Reports</h2>
|
<h3 className="text-sm font-normal text-secondary">Inventory Snapshot</h3>
|
||||||
<p className="text-xs text-slate-500">
|
<p className="text-xs text-muted mt-0.5">
|
||||||
Download inventory snapshots and audit trails in CSV or Excel formats
|
Export current inventory state with all item details
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{/* Inventory Snapshot Section */}
|
|
||||||
<div className="mb-8 space-y-3">
|
|
||||||
<h3 className="font-normal text-slate-700">Inventory Snapshot</h3>
|
|
||||||
<p className="text-sm text-slate-500">
|
|
||||||
Export current inventory state with all item details
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-col gap-2 sm:flex-row">
|
|
||||||
<button
|
<button
|
||||||
onClick={() => handleExportSnapshot("csv")}
|
onClick={() => handleExportSnapshot("csv")}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="flex items-center justify-center gap-2 rounded-md bg-blue-500 px-4 py-2 text-sm font-normal text-white hover:bg-blue-600 disabled:bg-slate-400 disabled:cursor-not-allowed transition-colors"
|
className="bg-surface border border-slate-800 hover:border-primary/50 hover:bg-slate-800/50 text-white font-normal py-2.5 px-3 rounded-xl transition-all active:scale-[0.98] disabled:opacity-50 disabled:pointer-events-none flex items-center justify-center gap-2 text-xs focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none group"
|
||||||
aria-label="Export inventory snapshot as CSV"
|
aria-label="Export inventory snapshot as CSV"
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Loader2 size={16} className="animate-spin" />
|
<Loader2 size={16} className="animate-spin text-primary" />
|
||||||
) : (
|
) : (
|
||||||
<FileDown size={16} />
|
<FileText size={16} className="text-blue-400 group-hover:text-blue-300 transition-colors" />
|
||||||
)}
|
)}
|
||||||
{isLoading ? "Exporting..." : "Export as CSV"}
|
<span>CSV</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleExportSnapshot("xlsx")}
|
onClick={() => handleExportSnapshot("xlsx")}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="flex items-center justify-center gap-2 rounded-md bg-green-500 px-4 py-2 text-sm font-normal text-white hover:bg-green-600 disabled:bg-slate-400 disabled:cursor-not-allowed transition-colors"
|
className="bg-surface border border-slate-800 hover:border-primary/50 hover:bg-slate-800/50 text-white font-normal py-2.5 px-3 rounded-xl transition-all active:scale-[0.98] disabled:opacity-50 disabled:pointer-events-none flex items-center justify-center gap-2 text-xs focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none group"
|
||||||
aria-label="Export inventory snapshot as Excel"
|
aria-label="Export inventory snapshot as Excel"
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Loader2 size={16} className="animate-spin" />
|
<Loader2 size={16} className="animate-spin text-primary" />
|
||||||
) : (
|
) : (
|
||||||
<FileDown size={16} />
|
<FileSpreadsheet size={16} className="text-emerald-400 group-hover:text-emerald-300 transition-colors" />
|
||||||
)}
|
)}
|
||||||
{isLoading ? "Exporting..." : "Export as Excel"}
|
<span>Excel</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Audit Trail Section */}
|
{/* Audit Trail Section */}
|
||||||
<div className="space-y-3">
|
<div className="p-4 bg-slate-800/40 border border-slate-800 rounded-2xl space-y-4">
|
||||||
<h3 className="font-normal text-slate-700">Audit Trail</h3>
|
<div>
|
||||||
<p className="text-sm text-slate-500">
|
<h3 className="text-sm font-normal text-secondary">Audit Trail</h3>
|
||||||
Export complete audit log of all system actions and changes
|
<p className="text-xs text-muted mt-0.5">
|
||||||
</p>
|
Complete log of system actions and changes
|
||||||
<div className="flex flex-col gap-2 sm:flex-row">
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleExportAuditTrail("csv")}
|
onClick={() => handleExportAuditTrail("csv")}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="flex items-center justify-center gap-2 rounded-md bg-blue-500 px-4 py-2 text-sm font-normal text-white hover:bg-blue-600 disabled:bg-slate-400 disabled:cursor-not-allowed transition-colors"
|
className="bg-surface border border-slate-800 hover:border-primary/50 hover:bg-slate-800/50 text-white font-normal py-2.5 px-3 rounded-xl transition-all active:scale-[0.98] disabled:opacity-50 disabled:pointer-events-none flex items-center justify-center gap-2 text-xs focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none group"
|
||||||
aria-label="Export audit trail as CSV"
|
aria-label="Export audit trail as CSV"
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Loader2 size={16} className="animate-spin" />
|
<Loader2 size={16} className="animate-spin text-primary" />
|
||||||
) : (
|
) : (
|
||||||
<FileDown size={16} />
|
<FileText size={16} className="text-blue-400 group-hover:text-blue-300 transition-colors" />
|
||||||
)}
|
)}
|
||||||
{isLoading ? "Exporting..." : "Export as CSV"}
|
<span>CSV</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleExportAuditTrail("xlsx")}
|
onClick={() => handleExportAuditTrail("xlsx")}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="flex items-center justify-center gap-2 rounded-md bg-green-500 px-4 py-2 text-sm font-normal text-white hover:bg-green-600 disabled:bg-slate-400 disabled:cursor-not-allowed transition-colors"
|
className="bg-surface border border-slate-800 hover:border-primary/50 hover:bg-slate-800/50 text-white font-normal py-2.5 px-3 rounded-xl transition-all active:scale-[0.98] disabled:opacity-50 disabled:pointer-events-none flex items-center justify-center gap-2 text-xs focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none group"
|
||||||
aria-label="Export audit trail as Excel"
|
aria-label="Export audit trail as Excel"
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Loader2 size={16} className="animate-spin" />
|
<Loader2 size={16} className="animate-spin text-primary" />
|
||||||
) : (
|
) : (
|
||||||
<FileDown size={16} />
|
<FileSpreadsheet size={16} className="text-emerald-400 group-hover:text-emerald-300 transition-colors" />
|
||||||
)}
|
)}
|
||||||
{isLoading ? "Exporting..." : "Export as Excel"}
|
<span>Excel</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status Messages */}
|
|
||||||
{error && (
|
{error && (
|
||||||
<Toast
|
<div className="p-3 bg-rose-500/10 border border-rose-500/20 rounded-xl text-rose-500 text-[10px] flex items-center gap-2 animate-in fade-in zoom-in duration-300">
|
||||||
type="error"
|
<div className="w-1.5 h-1.5 bg-rose-500 rounded-full animate-pulse" />
|
||||||
message={`Export failed: ${error}`}
|
<span className="font-normal">Error: {error}</span>
|
||||||
onClose={() => {}}
|
</div>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{successMessage && (
|
|
||||||
<Toast type="success" message={successMessage} onClose={() => {}} />
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export function useExport(): UseExportReturn {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axiosInstance.get(
|
const response = await axiosInstance.get(
|
||||||
`/admin/db/export?format=${format}&type=inventory`,
|
`/admin/reports/export?format=${format}&type=inventory`,
|
||||||
{
|
{
|
||||||
responseType: "blob"
|
responseType: "blob"
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ export function useExport(): UseExportReturn {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axiosInstance.get(
|
const response = await axiosInstance.get(
|
||||||
`/admin/db/export?format=${format}&type=audit`,
|
`/admin/reports/export?format=${format}&type=audit`,
|
||||||
{
|
{
|
||||||
responseType: "blob"
|
responseType: "blob"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user