mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-05-19 08:28:48 +02:00
Fix analytics case() bug, add privacy mode, add prod DB sync script
All checks were successful
Deploy to VPS / deploy (push) Successful in 28s
All checks were successful
Deploy to VPS / deploy (push) Successful in 28s
Fix SQLAlchemy case() import in monthly-trend endpoint. Add data-sensitive attributes to Analytics charts and tables for privacy blur. Add scripts/sync-db.sh for one-click prod-to-local PostgreSQL sync. Remove SQLite artifacts from gitignore. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,6 +2,8 @@ node_modules/
|
|||||||
dist/
|
dist/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
*.db
|
||||||
|
*.db.bak
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ from typing import Optional
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from sqlalchemy import case
|
||||||
from sqlmodel import Session, func, select
|
from sqlmodel import Session, func, select
|
||||||
|
|
||||||
from app.auth import get_current_user
|
from app.auth import get_current_user
|
||||||
from app.db import get_session
|
from app.db import get_session
|
||||||
from app.models.models import Category, Transaction
|
from app.models.models import Category, Transaction
|
||||||
from app.api.v1.endpoints.transactions import get_cycle_range
|
from app.services.budget_projection import get_cycle_range
|
||||||
|
|
||||||
router = APIRouter(prefix="/analytics", tags=["analytics"])
|
router = APIRouter(prefix="/analytics", tags=["analytics"])
|
||||||
|
|
||||||
@@ -104,7 +105,7 @@ def monthly_trend(
|
|||||||
func.count(),
|
func.count(),
|
||||||
func.coalesce(
|
func.coalesce(
|
||||||
func.sum(
|
func.sum(
|
||||||
func.case(
|
case(
|
||||||
(Transaction.currency == "CRC", Transaction.amount),
|
(Transaction.currency == "CRC", Transaction.amount),
|
||||||
else_=0,
|
else_=0,
|
||||||
)
|
)
|
||||||
@@ -113,7 +114,7 @@ def monthly_trend(
|
|||||||
),
|
),
|
||||||
func.coalesce(
|
func.coalesce(
|
||||||
func.sum(
|
func.sum(
|
||||||
func.case(
|
case(
|
||||||
(Transaction.currency == "USD", Transaction.amount),
|
(Transaction.currency == "USD", Transaction.amount),
|
||||||
else_=0,
|
else_=0,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -46,9 +46,8 @@ interface DailySpending {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const COLORS = [
|
const COLORS = [
|
||||||
'var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)', 'var(--chart-5)',
|
'#B45309', '#16A34A', '#2563EB', '#DC2626', '#7C3AED',
|
||||||
'oklch(0.7 0.15 30)', 'oklch(0.65 0.2 300)', 'oklch(0.6 0.15 150)',
|
'#D97706', '#0F766E', '#DB2777', '#EA580C', '#4F46E5',
|
||||||
'oklch(0.75 0.12 60)', 'oklch(0.55 0.18 250)',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
function formatCRC(value: number) {
|
function formatCRC(value: number) {
|
||||||
@@ -132,7 +131,7 @@ export default function Analytics() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<ChartContainer config={pieChartConfig} className="h-[260px] w-full">
|
<ChartContainer data-sensitive config={pieChartConfig} className="h-[260px] w-full">
|
||||||
<PieChart>
|
<PieChart>
|
||||||
<Pie
|
<Pie
|
||||||
data={byCategory}
|
data={byCategory}
|
||||||
@@ -168,7 +167,7 @@ export default function Analytics() {
|
|||||||
style={{ background: COLORS[i % COLORS.length] }}
|
style={{ background: COLORS[i % COLORS.length] }}
|
||||||
/>
|
/>
|
||||||
<span className="text-muted-foreground truncate">{cat.category_name}</span>
|
<span className="text-muted-foreground truncate">{cat.category_name}</span>
|
||||||
<span className="text-muted-foreground/60 ml-auto">{cat.percentage}%</span>
|
<span data-sensitive className="text-muted-foreground/60 ml-auto">{cat.percentage}%</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -190,7 +189,7 @@ export default function Analytics() {
|
|||||||
No data
|
No data
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ChartContainer config={trendChartConfig} className="h-[300px] w-full">
|
<ChartContainer data-sensitive config={trendChartConfig} className="h-[300px] w-full">
|
||||||
<BarChart data={trend}>
|
<BarChart data={trend}>
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="label"
|
dataKey="label"
|
||||||
@@ -229,7 +228,7 @@ export default function Analytics() {
|
|||||||
No data for this period
|
No data for this period
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ChartContainer config={dailyChartConfig} className="h-[240px] w-full">
|
<ChartContainer data-sensitive config={dailyChartConfig} className="h-[240px] w-full">
|
||||||
<LineChart data={daily}>
|
<LineChart data={daily}>
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="date"
|
dataKey="date"
|
||||||
@@ -287,11 +286,11 @@ export default function Analytics() {
|
|||||||
style={{ background: COLORS[i % COLORS.length] }}
|
style={{ background: COLORS[i % COLORS.length] }}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm flex-1">{cat.category_name}</span>
|
<span className="text-sm flex-1">{cat.category_name}</span>
|
||||||
<span className="text-xs text-muted-foreground">{cat.count} txns</span>
|
<span data-sensitive className="text-xs text-muted-foreground">{cat.count} txns</span>
|
||||||
<span className="text-sm font-mono font-medium w-32 text-right">
|
<span data-sensitive className="text-sm font-mono font-medium w-32 text-right">
|
||||||
{formatCRC(cat.total)}
|
{formatCRC(cat.total)}
|
||||||
</span>
|
</span>
|
||||||
<div className="w-24 bg-muted rounded-full h-1.5">
|
<div data-sensitive className="w-24 bg-muted rounded-full h-1.5">
|
||||||
<div
|
<div
|
||||||
className="h-1.5 rounded-full"
|
className="h-1.5 rounded-full"
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
99
scripts/sync-db.sh
Executable file
99
scripts/sync-db.sh
Executable file
@@ -0,0 +1,99 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── Configuration ────────────────────────────────────────────────
|
||||||
|
PROD_SSH_ALIAS="production"
|
||||||
|
PROD_CONTAINER="wealthysmart-db-prod"
|
||||||
|
PROD_DB="wealthysmart"
|
||||||
|
PROD_USER="wealthy_user"
|
||||||
|
|
||||||
|
LOCAL_CONTAINER="wealthysmart-db-dev"
|
||||||
|
LOCAL_DB="wealthysmart"
|
||||||
|
LOCAL_USER="wealthy_user"
|
||||||
|
LOCAL_PASS="wealthy_pass"
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||||
|
DUMP_FILE="$(mktemp -t wealthysmart-dump-XXXXXX)"
|
||||||
|
|
||||||
|
# ── Cleanup on exit ─────────────────────────────────────────────
|
||||||
|
cleanup() { rm -f "$DUMP_FILE"; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# ── Confirmation ─────────────────────────────────────────────────
|
||||||
|
echo "=== WealthySmart Database Sync ==="
|
||||||
|
echo ""
|
||||||
|
echo "This will DESTROY your local dev database and replace it"
|
||||||
|
echo "with a copy of production data."
|
||||||
|
echo ""
|
||||||
|
read -r -p "Continue? [y/N] " confirm
|
||||||
|
if [[ "$confirm" != [yY] ]]; then
|
||||||
|
echo "Aborted."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 1. Dump production ──────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "[1/5] Dumping production database..."
|
||||||
|
ssh "$PROD_SSH_ALIAS" \
|
||||||
|
"docker exec $PROD_CONTAINER pg_dump \
|
||||||
|
--format=custom \
|
||||||
|
--no-owner \
|
||||||
|
--no-acl \
|
||||||
|
-U $PROD_USER \
|
||||||
|
$PROD_DB" > "$DUMP_FILE"
|
||||||
|
|
||||||
|
if [[ ! -s "$DUMP_FILE" ]]; then
|
||||||
|
echo "ERROR: Dump file is empty. SSH or pg_dump may have failed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DUMP_SIZE=$(du -h "$DUMP_FILE" | cut -f1)
|
||||||
|
echo " Done. Dump size: $DUMP_SIZE"
|
||||||
|
|
||||||
|
# ── 2. Ensure local DB container is running ─────────────────────
|
||||||
|
echo "[2/5] Ensuring local dev database is running..."
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
|
if ! docker inspect --format='{{.State.Running}}' "$LOCAL_CONTAINER" 2>/dev/null | grep -q "true"; then
|
||||||
|
echo " Starting db service..."
|
||||||
|
docker compose up -d db
|
||||||
|
fi
|
||||||
|
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
if docker inspect --format='{{.State.Health.Status}}' "$LOCAL_CONTAINER" 2>/dev/null | grep -q "healthy"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [[ $i -eq 30 ]]; then
|
||||||
|
echo "ERROR: Local DB container did not become healthy within 30s."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo " Local DB is running and healthy."
|
||||||
|
|
||||||
|
# ── 3. Drop and recreate local database ─────────────────────────
|
||||||
|
echo "[3/5] Dropping and recreating local dev database..."
|
||||||
|
docker exec "$LOCAL_CONTAINER" bash -c \
|
||||||
|
"PGPASSWORD='$LOCAL_PASS' dropdb -U $LOCAL_USER --if-exists $LOCAL_DB && \
|
||||||
|
PGPASSWORD='$LOCAL_PASS' createdb -U $LOCAL_USER $LOCAL_DB"
|
||||||
|
echo " Done."
|
||||||
|
|
||||||
|
# ── 4. Restore ──────────────────────────────────────────────────
|
||||||
|
echo "[4/5] Restoring dump into local dev database..."
|
||||||
|
docker exec -i "$LOCAL_CONTAINER" pg_restore \
|
||||||
|
--no-owner \
|
||||||
|
--no-acl \
|
||||||
|
--dbname="$LOCAL_DB" \
|
||||||
|
-U "$LOCAL_USER" < "$DUMP_FILE"
|
||||||
|
|
||||||
|
# ── 5. Run pending migrations ───────────────────────────────────
|
||||||
|
echo "[5/5] Running pending migrations..."
|
||||||
|
docker exec "$LOCAL_CONTAINER" psql -U "$LOCAL_USER" -d "$LOCAL_DB" -c \
|
||||||
|
"ALTER TABLE transaction ADD COLUMN IF NOT EXISTS deferred_to_next_cycle BOOLEAN NOT NULL DEFAULT false;" \
|
||||||
|
2>/dev/null || true
|
||||||
|
echo " Done."
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Sync complete! ==="
|
||||||
|
echo "Local dev database now mirrors production."
|
||||||
Reference in New Issue
Block a user