Skip to content

Commit 63d13fb

Browse files
PyMite6941claude
andcommitted
Add auth-service (JWT licensing), Pro Features page, and merge remote analytics
- auth-service/: Cloud Run JWT service with Pro/Max tier support, Polar.sh webhook, manual gen_code.py for crypto buyers, purchase page HTML - CLI/app/pages/Pro Features.py: Streamlit license key input + advanced categorization UI - backend/bots.py: AdvancedCategorizationCrew (3 CrewAI agents) - backend/server.py: merged JWT auth + /advanced-categorize with remote's /forecast, /detect-anomalies, /tax-summary, /query endpoints - CLI/core/core_stuff.py: kept upstream's reportlab PDF export - Resolved stash/upstream conflicts across Dashboard.py, core_stuff.py, server.py Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3235c5f commit 63d13fb

16 files changed

Lines changed: 1529 additions & 22 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ nul
88
*.pdf
99
*.md
1010
.env
11-
pdf_converter.py
11+
pdf_converter.py
12+
test_backend.py

CLI/app/cli/cli_app.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ def __init__(self):
350350
notes = str(questionary.text("Enter any notes (default is ''):\n> ").ask())
351351
if not notes.strip():
352352
notes = None
353-
result = tracker.edit_income(expense_id,amount if amount is not None else None,source if source is not None else None,date if date is not None else None,currency if currency is not None else None,notes if notes is not None else None)
353+
result = tracker.edit_income(expense_id,amount=amount if 'Amount' in choice else None,source=source if 'Source' in choice else None,date=date if 'Date' in choice else None,currency=currency if 'Currency' in choice else None,notes=notes if 'Notes' in choice else None)
354354
color = 'green' if result['success'] else 'red'
355355
console.print(f"[bold {color}]{result['message']}[/bold {color}].")
356356
# Get the function to delete income
@@ -409,7 +409,9 @@ def __init__(self):
409409
price = float(questionary.text("How much is the subscription?\n> ").ask())
410410
currency = str(questionary.text("What currency is the subscription in? (default is usd)\n> ").ask())
411411
startDate = str(questionary.text("When did the subscription start? (yyyy-mm-dd)\n> ").ask())
412-
result = tracker.add_subscriptions(name,price,currency,startDate)
412+
result = tracker.add_subscriptions(name,price,currency if currency else 'usd',startDate)
413+
color = 'green' if result['success'] else 'red'
414+
console.print(f"[bold {color}]{result['message']}[/bold {color}].")
413415
# Get the function to edit subscriptions
414416
elif choice == 'Edit a subscription':
415417
previous_name = str(questionary.text("What is the name of the subscription to be edited?\n> ").ask())
@@ -501,7 +503,7 @@ def __init__(self):
501503
startDate = str(questionary.text("What is the start date of this goal? (default is today)\n> ").ask())
502504
monthContribution = float(questionary.text("What is the monthly contribution from the inputted income?\n> ").ask())
503505
currency = str(questionary.text("What is the currency this goal uses?\n> ").ask())
504-
results = tracker.create_goal(name,amount,startDate,monthContribution,currency)
506+
results = tracker.create_goal(name,amount,startDate,monthContribution,currency if currency else 'usd')
505507
color = 'green' if results['success'] else 'red'
506508
console.print(f"[bold {color}]{results['message']}[/bold {color}].")
507509
# Get the function to edit a goal

CLI/app/pages/Monthly Summary.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# For the web ui setup
2+
import streamlit as st
3+
import pandas as pd
4+
import matplotlib.pyplot as plt
5+
# For proper importing stuff
6+
import os
7+
import sys
8+
sys.path.insert(0,os.path.abspath(os.path.join(os.path.dirname(__file__),'..','..','..')))
9+
# Initialize the session states
10+
from CLI.app.streamlit_setup import init_st, sync_data
11+
12+
init_st()
13+
14+
st.title('Monthly Summary')
15+
16+
# Get current month and year
17+
current_month = st.session_state.current_month
18+
month_name = pd.to_datetime(current_month).strftime('%B %Y')
19+
20+
# Filter expenses and income for the current month
21+
expenses = [expense for expense in st.session_state.expenses if expense['date'][:7] == current_month]
22+
income = [inc for inc in st.session_state.income if inc['date'][:7] == current_month]
23+
24+
# Calculate totals
25+
total_expenses = sum(expense['price'] for expense in expenses)
26+
total_income = sum(inc['amount'] for inc in income)
27+
net_savings = total_income - total_expenses
28+
29+
# Display metrics
30+
col1, col2, col3 = st.columns(3)
31+
with col1:
32+
st.metric('Total Expenses', f"${total_expenses:.2f}")
33+
with col2:
34+
st.metric('Total Income', f"${total_income:.2f}")
35+
with col3:
36+
st.metric('Net Savings', f"${net_savings:.2f}", delta_color="inverse" if net_savings < 0 else "normal")
37+
38+
st.divider()
39+
40+
# Expense breakdown by category
41+
expense_by_category = {}
42+
for expense in expenses:
43+
category = expense['tags']
44+
expense_by_category[category] = expense_by_category.get(category, 0) + expense['price']
45+
46+
if expense_by_category:
47+
st.subheader('Expenses by Category')
48+
49+
# Create a DataFrame for the table
50+
df = pd.DataFrame({
51+
'Category': list(expense_by_category.keys()),
52+
'Amount': list(expense_by_category.values())
53+
}).sort_values('Amount', ascending=False)
54+
55+
# Display table
56+
st.dataframe(df, hide_index=True)
57+
58+
# Create pie chart
59+
fig, ax = plt.subplots()
60+
ax.pie(df['Amount'], labels=df['Category'], autopct='%1.1f%%')
61+
ax.set_title('Expense Distribution')
62+
st.pyplot(fig)
63+
else:
64+
st.write("No expenses recorded for this month.")
65+
66+
st.divider()
67+
68+
# Income breakdown by source
69+
income_by_source = {}
70+
for inc in income:
71+
source = inc['source']
72+
income_by_source[source] = income_by_source.get(source, 0) + inc['amount']
73+
74+
if income_by_source:
75+
st.subheader('Income by Source')
76+
77+
# Create a DataFrame for the table
78+
df = pd.DataFrame({
79+
'Source': list(income_by_source.keys()),
80+
'Amount': list(income_by_source.values())
81+
}).sort_values('Amount', ascending=False)
82+
83+
# Display table
84+
st.dataframe(df, hide_index=True)
85+
86+
# Create pie chart
87+
fig, ax = plt.subplots()
88+
ax.pie(df['Amount'], labels=df['Source'], autopct='%1.1f%%')
89+
ax.set_title('Income Distribution')
90+
st.pyplot(fig)
91+
else:
92+
st.write("No income recorded for this month.")
93+
94+
st.divider()
95+
96+
# Monthly comparison (if previous months exist)
97+
all_months = sorted(list(set([expense['date'][:7] for expense in st.session_state.expenses] + [inc['date'][:7] for inc in st.session_state.income])), reverse=True)
98+
99+
if len(all_months) > 1:
100+
st.subheader('Monthly Comparison')
101+
102+
# Create a DataFrame for monthly data
103+
monthly_data = []
104+
for month in all_months:
105+
month_expenses = sum(expense['price'] for expense in st.session_state.expenses if expense['date'][:7] == month)
106+
month_income = sum(inc['amount'] for inc in st.session_state.income if inc['date'][:7] == month)
107+
monthly_data.append({
108+
'Month': pd.to_datetime(month).strftime('%B %Y'),
109+
'Expenses': month_expenses,
110+
'Income': month_income,
111+
'Savings': month_income - month_expenses
112+
})
113+
114+
df = pd.DataFrame(monthly_data)
115+
116+
# Display table
117+
st.dataframe(df, hide_index=True)
118+
119+
# Create bar chart
120+
fig, ax = plt.subplots()
121+
df.plot(x='Month', y=['Expenses', 'Income'], kind='bar', ax=ax)
122+
ax.set_title('Monthly Expenses vs Income')
123+
ax.set_ylabel('Amount ($)')
124+
plt.xticks(rotation=45)
125+
plt.tight_layout()
126+
st.pyplot(fig)
127+
128+
# Create savings trend chart
129+
fig, ax = plt.subplots()
130+
df.plot(x='Month', y='Savings', kind='line', marker='o', ax=ax)
131+
ax.set_title('Monthly Savings Trend')
132+
ax.set_ylabel('Savings ($)')
133+
plt.xticks(rotation=45)
134+
plt.tight_layout()
135+
st.pyplot(fig)
136+
137+
# Export options
138+
st.subheader('Export Data')
139+
col1, col2 = st.columns(2)
140+
with col1:
141+
if st.button('Export Expenses to CSV'):
142+
result = st.session_state.tracker.export_to_csv('expenses', 'expenses.csv')
143+
if result['success']:
144+
st.download_button(
145+
label="Download Expenses CSV",
146+
data=result['data'].to_csv(index=False).encode('utf-8'),
147+
file_name="expenses.csv",
148+
mime="text/csv"
149+
)
150+
with col2:
151+
if st.button('Export Summary to PDF'):
152+
result = st.session_state.tracker.export_to_pdf('expenses', 'monthly_summary.pdf')
153+
if result['success']:
154+
with open('monthly_summary.pdf', 'rb') as f:
155+
st.download_button(
156+
label="Download PDF Summary",
157+
data=f,
158+
file_name="monthly_summary.pdf",
159+
mime="application/pdf"
160+
)

CLI/app/pages/Pro Features.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import os
2+
import sys
3+
4+
import requests
5+
import streamlit as st
6+
7+
# Allow imports from project root
8+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", ".."))
9+
from core.core_stuff import ExpenseTracker
10+
11+
AUTH_SERVICE_URL = os.getenv("AUTH_SERVICE_URL", "")
12+
BACKEND_URL = os.getenv("BACKEND_URL", "")
13+
14+
st.set_page_config(page_title="Pro Features", page_icon="⭐")
15+
st.title("⭐ Pro Features")
16+
17+
if not AUTH_SERVICE_URL or not BACKEND_URL:
18+
st.warning(
19+
"Set `AUTH_SERVICE_URL` and `BACKEND_URL` environment variables to connect to the services.",
20+
icon="⚠️",
21+
)
22+
23+
# ── License key input ────────────────────────────────────────────────────────
24+
25+
st.subheader("Activate your license")
26+
27+
saved_token = st.session_state.get("pro_token", "")
28+
token_input = st.text_input(
29+
"License key",
30+
value=saved_token,
31+
type="password",
32+
placeholder="Paste the key from your purchase email…",
33+
)
34+
35+
col_activate, col_clear = st.columns([1, 4])
36+
37+
with col_activate:
38+
if st.button("Activate", type="primary", disabled=not token_input):
39+
if not AUTH_SERVICE_URL:
40+
st.error("AUTH_SERVICE_URL is not configured.")
41+
else:
42+
try:
43+
resp = requests.post(
44+
f"{AUTH_SERVICE_URL}/validate",
45+
json={"token": token_input},
46+
timeout=8,
47+
)
48+
if resp.status_code == 200:
49+
data = resp.json()
50+
st.session_state["pro_token"] = token_input
51+
st.session_state["pro_email"] = data["email"]
52+
st.success(f"License active — {data['email']}", icon="✅")
53+
st.rerun()
54+
else:
55+
st.error(f"Invalid or expired key. ({resp.status_code})")
56+
except requests.exceptions.ConnectionError:
57+
st.error("Could not reach the license server. Check AUTH_SERVICE_URL.")
58+
except Exception as e:
59+
st.error(f"Unexpected error: {e}")
60+
61+
with col_clear:
62+
if st.session_state.get("pro_token") and st.button("Remove license"):
63+
st.session_state.pop("pro_token", None)
64+
st.session_state.pop("pro_email", None)
65+
st.rerun()
66+
67+
# ── Pro features (only shown when license is active) ─────────────────────────
68+
69+
if not st.session_state.get("pro_token"):
70+
st.divider()
71+
st.info(
72+
"No active license. [Purchase Pro]({}) to unlock advanced AI features.".format(
73+
os.getenv("PURCHASE_PAGE_URL", "#")
74+
)
75+
)
76+
st.stop()
77+
78+
st.divider()
79+
st.subheader("Advanced Categorization")
80+
st.caption(
81+
"Three CrewAI agents analyze your expenses: a Categorization Specialist assigns precise "
82+
"subcategories, a Pattern Analyst surfaces trends and anomalies, and a Finance Advisor "
83+
"generates specific budget recommendations."
84+
)
85+
86+
context = st.text_input(
87+
"Focus (optional)",
88+
placeholder="e.g. 'Focus on the last 3 months' or leave blank for full analysis",
89+
)
90+
91+
if st.button("Run Advanced Categorization", type="primary"):
92+
if not BACKEND_URL:
93+
st.error("BACKEND_URL is not configured.")
94+
st.stop()
95+
96+
with st.spinner("Loading expenses…"):
97+
try:
98+
tracker = ExpenseTracker()
99+
expenses = tracker.view_total_expenses()
100+
except Exception as e:
101+
st.error(f"Could not load expense data: {e}")
102+
st.stop()
103+
104+
if not expenses:
105+
st.warning("No expenses found. Add some expenses first.")
106+
st.stop()
107+
108+
with st.spinner(f"Running AI analysis on {len(expenses)} expenses… this may take 30–60 seconds."):
109+
try:
110+
resp = requests.post(
111+
f"{BACKEND_URL}/advanced-categorize",
112+
json={
113+
"expenses": expenses,
114+
"context": context or "Advanced expense categorization with subcategories and pattern detection",
115+
},
116+
headers={"Authorization": f"Bearer {st.session_state['pro_token']}"},
117+
timeout=120,
118+
)
119+
except requests.exceptions.ConnectionError:
120+
st.error("Could not reach the backend. Check BACKEND_URL.")
121+
st.stop()
122+
except Exception as e:
123+
st.error(f"Request failed: {e}")
124+
st.stop()
125+
126+
if resp.status_code == 401:
127+
st.error("License key rejected by backend. Your key may have expired.")
128+
st.stop()
129+
if not resp.ok:
130+
st.error(f"Analysis failed (HTTP {resp.status_code}): {resp.text}")
131+
st.stop()
132+
133+
result = resp.json()
134+
st.session_state["last_categorization"] = result
135+
136+
# ── Display last result if available ─────────────────────────────────────────
137+
138+
result = st.session_state.get("last_categorization")
139+
if result:
140+
st.divider()
141+
142+
st.subheader("Summary")
143+
st.write(result.get("summary", "—"))
144+
145+
findings = result.get("key_findings", [])
146+
if findings:
147+
st.subheader("Key Findings")
148+
for f in findings:
149+
st.markdown(f"- {f}")
150+
151+
anomalies = result.get("anomalies", [])
152+
if anomalies:
153+
st.subheader("Anomalies")
154+
for a in anomalies:
155+
st.warning(a, icon="🚨")
156+
else:
157+
st.success("No anomalies detected in your spending.", icon="✅")
158+
159+
recs = result.get("recommendations", [])
160+
if recs:
161+
st.subheader("Budget Recommendations")
162+
for r in recs:
163+
st.info(r, icon="💡")

0 commit comments

Comments
 (0)