Skip to content

Commit d1acade

Browse files
PyMite6941claude
andcommitted
Add backend config file, route dashboard analytics through Cloud Run
- New CLI/app/config.py: BACKEND_MODE = 'cloud' (default) or 'local' - streamlit_setup.py reads config to set BACKEND_URL / AUTH_SERVICE_URL - Dashboard.py replaces all direct backend.* imports with HTTP calls to Cloud Run; falls back to local imports only when USE_LOCAL_BACKEND=True - backend/server.py: add /net-worth endpoint so dashboard can call it Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6e0e391 commit d1acade

4 files changed

Lines changed: 153 additions & 80 deletions

File tree

CLI/app/Dashboard.py

Lines changed: 120 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@
66
sys.path.insert(0,os.path.abspath(os.path.join(os.path.dirname(__file__),'..', '..')))
77

88
# Initialize the session states
9-
from CLI.app.streamlit_setup import init_st, sync_data
9+
from CLI.app.streamlit_setup import init_st, sync_data, BACKEND_URL, USE_LOCAL_BACKEND
10+
import requests as _requests
1011

1112
init_st()
1213

14+
15+
def _backend_post(endpoint: str, payload: dict):
16+
return _requests.post(f'{st.session_state.get("backend_url", BACKEND_URL)}{endpoint}', json=payload, timeout=30)
17+
1318
st.title('Web-based Expense and Income Tracking')
1419

1520
tab_dashboard, tab_add, tab_delete, tab_edit, tab_view_expenses, tab_view_income, tab_view_subscriptions = st.tabs([
@@ -32,33 +37,48 @@
3237
else:
3338
st.write("No income found.")
3439

35-
budget_totals = {}
36-
for expense in st.session_state.expenses:
37-
if expense['date'][:7] == st.session_state.current_month:
38-
tag = expense['tags']
39-
budget_totals[tag] = budget_totals.get(tag, 0) + expense['price']
40-
for budget in st.session_state.budget:
41-
total_spent = budget_totals.get(budget['category'], 0)
42-
limit = float(budget['amount'])
43-
if limit < total_spent:
44-
st.warning(f"{budget['category']} budget has been surpassed by {total_spent - limit:.2f}.")
45-
elif limit == total_spent:
46-
st.warning(f"{budget['category']} budget has reached its limit.")
40+
if st.session_state.budget:
41+
st.subheader('Budget Status')
42+
budget_totals = {}
43+
for expense in st.session_state.expenses:
44+
if expense['date'][:7] == st.session_state.current_month:
45+
tag = expense['tags']
46+
budget_totals[tag] = budget_totals.get(tag, 0) + expense['price']
47+
for budget in st.session_state.budget:
48+
total_spent = budget_totals.get(budget['category'], 0)
49+
limit = float(budget['amount'])
50+
pct = min(total_spent / limit, 1.0) if limit > 0 else 0.0
51+
over = total_spent > limit
52+
col_label, col_bar = st.columns([1, 3])
53+
with col_label:
54+
st.write(f"**{budget['category']}**")
55+
st.caption(f"{total_spent:.2f} / {limit:.2f} {budget.get('currency','USD').upper()}")
56+
with col_bar:
57+
st.progress(pct)
58+
if over:
59+
st.error(f"Over by {total_spent - limit:.2f} — reduce {budget['category']} spending", icon='🚨')
60+
elif pct >= 0.9:
61+
st.warning(f"{int(pct*100)}% used — approaching limit", icon='⚠️')
4762

4863
st.divider()
4964

5065
# ── Net-worth snapshot ───────────────────────────────────────────────────
5166
st.subheader('Net-Worth Snapshot')
5267
try:
53-
from backend.analytics import net_worth_snapshot
54-
_nw_data = {
55-
'expenses': st.session_state.expenses,
56-
'income': st.session_state.income,
57-
'subscriptions': st.session_state.subscriptions,
58-
'goals': st.session_state.goals,
59-
}
60-
_nw = net_worth_snapshot(_nw_data, convert_fn=st.session_state.tracker.convert_currency)
61-
if _nw['success']:
68+
if USE_LOCAL_BACKEND:
69+
from backend.analytics import net_worth_snapshot
70+
_nw = net_worth_snapshot(
71+
{'expenses': st.session_state.expenses, 'income': st.session_state.income,
72+
'subscriptions': st.session_state.subscriptions, 'goals': st.session_state.goals},
73+
convert_fn=st.session_state.tracker.convert_currency,
74+
)
75+
else:
76+
_resp = _backend_post('/net-worth', {
77+
'expenses': st.session_state.expenses, 'income': st.session_state.income,
78+
'subscriptions': st.session_state.subscriptions, 'goals': st.session_state.goals,
79+
})
80+
_nw = _resp.json() if _resp.ok else {'success': False}
81+
if _nw.get('success'):
6282
_nw_cols = st.columns(4)
6383
_nw_cols[0].metric('Total Income', f"{_nw['total_income']:,.2f} {_nw['base_currency']}")
6484
_nw_cols[1].metric('Total Expenses', f"{_nw['total_expenses']:,.2f} {_nw['base_currency']}")
@@ -75,20 +95,19 @@
7595
# ── Spending forecast ────────────────────────────────────────────────────
7696
st.subheader('Spending Forecast')
7797
try:
78-
from backend.analytics import forecast_spending
79-
_fc = forecast_spending(st.session_state.expenses)
80-
if _fc['success'] and _fc['forecasts']:
98+
if USE_LOCAL_BACKEND:
99+
from backend.analytics import forecast_spending
100+
_fc = forecast_spending(st.session_state.expenses)
101+
else:
102+
_resp = _backend_post('/forecast', {'expenses': st.session_state.expenses, 'base_currency': 'USD'})
103+
_fc = _resp.json() if _resp.ok else {'success': False, 'forecasts': {}}
104+
if _fc.get('success') and _fc.get('forecasts'):
81105
st.caption(f"Based on {_fc['based_on_months']} month(s) of history ({_fc['base_currency']} only)")
82106
_fc_cols = st.columns(3)
83107
for _fi, (_cat, _info) in enumerate(_fc['forecasts'].items()):
84-
_col = _fc_cols[_fi % 3]
85108
_arrow = '↑' if _info['trend'] == 'increasing' else '↓' if _info['trend'] == 'decreasing' else '→'
86-
_col.metric(
87-
f'{_cat} {_arrow}',
88-
f"{_info['next_month_forecast']:,.2f}",
89-
delta=f"avg {_info['current_avg']:,.2f}",
90-
)
91-
elif not _fc['success']:
109+
_fc_cols[_fi % 3].metric(f'{_cat} {_arrow}', f"{_info['next_month_forecast']:,.2f}", delta=f"avg {_info['current_avg']:,.2f}")
110+
else:
92111
st.info('Not enough expense history to generate a forecast yet.')
93112
except Exception as _e:
94113
st.info(f'Forecast unavailable: {_e}')
@@ -98,9 +117,13 @@
98117
# ── Anomaly detection ────────────────────────────────────────────────────
99118
st.subheader('Unusual Expenses')
100119
try:
101-
from backend.analytics import detect_anomalies
102-
_ad = detect_anomalies(st.session_state.expenses)
103-
if _ad['anomalies']:
120+
if USE_LOCAL_BACKEND:
121+
from backend.analytics import detect_anomalies
122+
_ad = detect_anomalies(st.session_state.expenses)
123+
else:
124+
_resp = _backend_post('/detect-anomalies', {'expenses': st.session_state.expenses, 'z_threshold': 2.5})
125+
_ad = _resp.json() if _resp.ok else {'anomalies': []}
126+
if _ad.get('anomalies'):
104127
st.caption(f"{_ad['count']} statistically unusual expense(s) detected:")
105128
for _anom in _ad['anomalies']:
106129
_dev_sign = '+' if _anom['deviation'] > 0 else ''
@@ -119,30 +142,26 @@
119142

120143
# ── Natural language query ───────────────────────────────────────────────
121144
st.subheader('Ask About Your Finances')
122-
try:
123-
from backend.ai import is_configured as _ai_ok, answer_query as _answer_query
124-
if not _ai_ok():
125-
st.info('Set AI_API_KEY (and optionally AI_PROVIDER, AI_MODEL) to enable natural-language queries.')
126-
else:
127-
_nl_question = st.text_input('Ask anything about your finances …', key='nl_query_input')
128-
if st.button('Ask', key='nl_query_btn') and _nl_question.strip():
129-
with st.spinner('Thinking …'):
130-
_nl_data = {
131-
'expenses': st.session_state.expenses,
132-
'income': st.session_state.income,
133-
'budget': st.session_state.budget,
134-
'subscriptions': st.session_state.subscriptions,
135-
'goals': st.session_state.goals,
136-
}
137-
try:
138-
_nl_answer = _answer_query(_nl_question, _nl_data)
139-
st.session_state['nl_last_answer'] = _nl_answer
140-
except Exception as _exc:
141-
st.error(f'Query failed: {_exc}')
142-
if st.session_state.get('nl_last_answer'):
143-
st.markdown(st.session_state['nl_last_answer'])
144-
except Exception as _e:
145-
st.info(f'AI query unavailable: {_e}')
145+
_nl_question = st.text_input('Ask anything about your finances …', key='nl_query_input')
146+
if st.button('Ask', key='nl_query_btn') and _nl_question.strip():
147+
with st.spinner('Thinking …'):
148+
_nl_data = {
149+
'expenses': st.session_state.expenses, 'income': st.session_state.income,
150+
'budget': st.session_state.budget, 'subscriptions': st.session_state.subscriptions,
151+
'goals': st.session_state.goals,
152+
}
153+
try:
154+
if USE_LOCAL_BACKEND:
155+
from backend.ai import answer_query as _answer_query
156+
_nl_answer = _answer_query(_nl_question, _nl_data)
157+
else:
158+
_resp = _backend_post('/query', {'question': _nl_question, 'data': _nl_data})
159+
_nl_answer = _resp.json().get('answer', _resp.text) if _resp.ok else f'Error {_resp.status_code}'
160+
st.session_state['nl_last_answer'] = _nl_answer
161+
except Exception as _exc:
162+
st.error(f'Query failed: {_exc}')
163+
if st.session_state.get('nl_last_answer'):
164+
st.markdown(st.session_state['nl_last_answer'])
146165

147166
# ── Add ──────────────────────────────────────────────────────────────────────
148167
with tab_add:
@@ -511,12 +530,20 @@
511530
else:
512531
st.write("No expenses found. Add expenses to get started.")
513532

514-
st.file_uploader('Import expenses from .csv', type=['csv'], key='expenses_file_uploader')
515-
if st.session_state.get('expenses_file_uploader'):
516-
st.session_state.tracker.import_from_csv('expenses', st.session_state.expenses_file_uploader)
517-
sync_data()
518-
st.success('Data imported successfully!')
519-
st.rerun()
533+
with st.form('import_expenses_form', clear_on_submit=True):
534+
uploaded_csv = st.file_uploader('Import expenses from .csv', type=['csv'], key='expenses_csv_upload')
535+
if st.form_submit_button('Import') and uploaded_csv:
536+
import pandas as _pd
537+
try:
538+
preview = _pd.read_csv(uploaded_csv)
539+
st.dataframe(preview.head(3))
540+
uploaded_csv.seek(0)
541+
st.session_state.tracker.import_from_csv('expenses', uploaded_csv)
542+
sync_data()
543+
st.success(f'Imported {len(preview)} rows successfully.')
544+
st.rerun()
545+
except Exception as _e:
546+
st.error(f'Import failed: {_e}')
520547
result = st.session_state.tracker.export_to_csv('expenses', 'expenses.csv')
521548
if result['success']:
522549
st.download_button(label='Export expenses to .csv', data=result['data'].to_csv(index=False).encode('utf-8'), file_name='expenses.csv', mime='text/csv', key='exp_download')
@@ -550,12 +577,20 @@
550577
else:
551578
st.write("No income found. Add income to get started.")
552579

553-
st.file_uploader('Import income from .csv', type=['csv'], key='income_file_uploader')
554-
if st.session_state.get('income_file_uploader'):
555-
st.session_state.tracker.import_from_csv('income', st.session_state.income_file_uploader)
556-
sync_data()
557-
st.success('Data imported successfully!')
558-
st.rerun()
580+
with st.form('import_income_form', clear_on_submit=True):
581+
uploaded_csv = st.file_uploader('Import income from .csv', type=['csv'], key='income_csv_upload')
582+
if st.form_submit_button('Import') and uploaded_csv:
583+
import pandas as _pd
584+
try:
585+
preview = _pd.read_csv(uploaded_csv)
586+
st.dataframe(preview.head(3))
587+
uploaded_csv.seek(0)
588+
st.session_state.tracker.import_from_csv('income', uploaded_csv)
589+
sync_data()
590+
st.success(f'Imported {len(preview)} rows successfully.')
591+
st.rerun()
592+
except Exception as _e:
593+
st.error(f'Import failed: {_e}')
559594
result = st.session_state.tracker.export_to_csv('income', 'income.csv')
560595
if result['success']:
561596
st.download_button(label='Export income to .csv', data=result['data'].to_csv(index=False).encode('utf-8'), file_name='income.csv', mime='text/csv', key='inc_download')
@@ -584,12 +619,20 @@
584619
else:
585620
st.write("No subscriptions found. Add subscriptions to get started.")
586621

587-
st.file_uploader('Import subscriptions from .csv', type=['csv'], key='subscriptions_file_uploader')
588-
if st.session_state.get('subscriptions_file_uploader'):
589-
st.session_state.tracker.import_from_csv('subscriptions', st.session_state.subscriptions_file_uploader)
590-
sync_data()
591-
st.success('Data imported successfully!')
592-
st.rerun()
622+
with st.form('import_subscriptions_form', clear_on_submit=True):
623+
uploaded_csv = st.file_uploader('Import subscriptions from .csv', type=['csv'], key='subscriptions_csv_upload')
624+
if st.form_submit_button('Import') and uploaded_csv:
625+
import pandas as _pd
626+
try:
627+
preview = _pd.read_csv(uploaded_csv)
628+
st.dataframe(preview.head(3))
629+
uploaded_csv.seek(0)
630+
st.session_state.tracker.import_from_csv('subscriptions', uploaded_csv)
631+
sync_data()
632+
st.success(f'Imported {len(preview)} rows successfully.')
633+
st.rerun()
634+
except Exception as _e:
635+
st.error(f'Import failed: {_e}')
593636
result = st.session_state.tracker.export_to_csv('subscriptions', 'subscriptions.csv')
594637
if result['success']:
595638
st.download_button(label='Export subscriptions to .csv', data=result['data'].to_csv(index=False).encode('utf-8'), file_name='subscriptions.csv', mime='text/csv', key='sub_download')

CLI/app/config.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Backend mode — "cloud" uses Google Cloud Run (default), "local" runs the backend on this machine
2+
BACKEND_MODE = "cloud"
3+
4+
CLOUD_BACKEND_URL = "https://expense-backend-690527435721.us-central1.run.app"
5+
CLOUD_AUTH_URL = "https://auth-service-690527435721.us-central1.run.app"
6+
7+
LOCAL_BACKEND_URL = "http://localhost:8000"
8+
LOCAL_AUTH_URL = "http://localhost:8001"

CLI/app/streamlit_setup.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,16 @@
77
# Get the class to use
88
from CLI.core.core_stuff import ExpenseTracker
99

10-
BACKEND_URL = os.getenv("BACKEND_URL", "https://expense-backend-690527435721.us-central1.run.app")
11-
AUTH_SERVICE_URL = os.getenv("AUTH_SERVICE_URL", "https://auth-service-690527435721.us-central1.run.app")
10+
from CLI.app.config import (
11+
BACKEND_MODE,
12+
CLOUD_BACKEND_URL, CLOUD_AUTH_URL,
13+
LOCAL_BACKEND_URL, LOCAL_AUTH_URL,
14+
)
15+
16+
_local = BACKEND_MODE == "local"
17+
BACKEND_URL = os.getenv("BACKEND_URL", LOCAL_BACKEND_URL if _local else CLOUD_BACKEND_URL)
18+
AUTH_SERVICE_URL = os.getenv("AUTH_SERVICE_URL", LOCAL_AUTH_URL if _local else CLOUD_AUTH_URL)
19+
USE_LOCAL_BACKEND = _local
1220

1321
# Initialize the session states
1422
def init_st():

backend/server.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from jose import JWTError, jwt
77
from pydantic import BaseModel
88

9-
from analytics import detect_anomalies, forecast_spending, tax_summary
9+
from analytics import detect_anomalies, forecast_spending, net_worth_snapshot, tax_summary
1010
from ai import answer_query, is_configured as ai_configured
1111
from bots import AdvancedCategorizationCrew
1212
from ocr import parse_receipt
@@ -48,6 +48,13 @@ class TaxSummaryRequest(BaseModel):
4848
deductible_categories: Optional[list] = None
4949

5050

51+
class NetWorthRequest(BaseModel):
52+
expenses: list
53+
income: list
54+
subscriptions: list = []
55+
goals: list = []
56+
57+
5158
class QueryRequest(BaseModel):
5259
question: str
5360
data: dict
@@ -75,6 +82,13 @@ async def parse(file: UploadFile):
7582
raise HTTPException(status_code=500, detail='Receipt parsing failed. Try again.')
7683

7784

85+
@app.post('/net-worth')
86+
def net_worth(req: NetWorthRequest):
87+
data = {'expenses': req.expenses, 'income': req.income,
88+
'subscriptions': req.subscriptions, 'goals': req.goals}
89+
return net_worth_snapshot(data)
90+
91+
7892
@app.post('/forecast')
7993
def forecast(req: ForecastRequest):
8094
result = forecast_spending(req.expenses, base_currency=req.base_currency)

0 commit comments

Comments
 (0)