Customer Analytics Dashboard···
ExplorerChangesTerminalBrowserLLM trace
Build a customer analytics dashboard with charts, filters, and date ranges.
I’ll build a reviewable analytics dashboard with data models, metrics APIs, interactive charts, and date-range filtering.
0 / 5 tasks done
Set up project structure
Add event, user, and metric models
Create metrics, filter, and export APIs
Build dashboard and chart detail views
Add setup docs and tests
Build summary
18 files changed
5 pages created
4 API routes added
12 tests passed
All tasks are complete. The analytics dashboard is ready for review.
Ask for follow-up changes
Loading diff…
No files changed
customer-analytics-dashboard
▾customer-analytics-dashboard
▾backend
▾app
__init__.py
database.py
models.py
main.py
schemas.py
risk_engine.py
eligibility_engine.py
▾routes
applications.py
documents.py
requirements.txt
▾tests
test_applications.py
test_risk_engine.py
▾frontend
package.json
vite.config.ts
index.html
▾src
App.tsx
▾pages
ApplicationForm.tsx
Dashboard.tsx
LoanDetails.tsx
AmortizationView.tsx
README.md
.env.example
1”””
2Loan Approval System – FastAPI Application
3===========================================
4Private-lender personal loan management with:
5 • Borrower application intake
6 • Credit / risk assessment engine
7 • Auto-approval eligibility rules
8 • Document upload & management
9 • Amortization schedule generation
10 • Dashboard analytics
11”””
12
13from __future__ import annotations
14
15import os
16import uuid
17from typing import List
18
19from fastapi import FastAPI, Depends, HTTPException, UploadFile, File, Query
20from fastapi.middleware.cors import CORSMiddleware
21from fastapi.staticfiles import StaticFiles
22from sqlalchemy.orm import Session
23from sqlalchemy import func
24
25from .database import Base, engine, get_db
26from .models import Borrower, Loan, Document, AmortizationEntry, LoanStatus
27from .schemas import (
28 BorrowerOut,
29 LoanApplicationCreate,
30 LoanOut,
31 LoanStatusUpdate,
32 RiskAssessmentOut,
33 AmortizationEntryOut,
34 DocumentOut,
35 DashboardStats,
36)
37from .risk_engine import assess_risk
38from .amortization import generate_schedule
39
40# ---------------------------------------------------------------------------
41# Bootstrap
42# ---------------------------------------------------------------------------
43
44Base.metadata.create_all(bind=engine)
45
46UPLOAD_DIR = os.path.join(os.path.dirname(__file__), ”..”, ”uploads”)
47os.makedirs(UPLOAD_DIR, exist_ok=True)
48
49app = FastAPI(
50 title=”Loan Approval System”,
51 version=”1.0.0”,
52 description=”Private-lender personal loan management API”,
53)
54
55app.add_middleware(
56 CORSMiddleware,
57 allow_origins=[”*”],
58 allow_credentials=True,
59 allow_methods=[”*”],
60 allow_headers=[”*”],
61)
62
63
64# ---------------------------------------------------------------------------
65# HEALTH
66# ---------------------------------------------------------------------------
67
68@app.get(”/api/health”)
69def health_check():
70 return {”status”: ”ok”}
71
72
73# ---------------------------------------------------------------------------
74# LOAN APPLICATION (combined borrower + loan creation)
75# ---------------------------------------------------------------------------
76
77@app.post(”/api/applications”, response_model=LoanOut, status_code=201)
78def create_application(payload: LoanApplicationCreate, db: Session = Depends(get_db)):
79 ”””Submit a new loan application (creates borrower if needed).”””
80
81 # Upsert borrower by email
82 borrower = db.query(Borrower).filter(Borrower.email == payload.email).first()
83 if borrower:
84 # Update mutable fields
85 for field in (
86 ”first_name”, ”last_name”, ”phone”, ”date_of_birth”, ”address”,
87 ”employment_type”, ”employer_name”, ”monthly_income”,
88 ”existing_debt”, ”credit_score”,
89 ):
90 setattr(borrower, field, getattr(payload, field))
91 else:
92 borrower = Borrower(
93 first_name=payload.first_name,
94 last_name=payload.last_name,
95 email=payload.email,
96 phone=payload.phone,
97 date_of_birth=payload.date_of_birth,
98 address=payload.address,
99 employment_type=payload.employment_type,
100 employer_name=payload.employer_name,
101 monthly_income=payload.monthly_income,
102 existing_debt=payload.existing_debt,
103 credit_score=payload.credit_score,
104 )
105 db.add(borrower)
106 db.flush()
107
108 # Run risk assessment
109 assessment = assess_risk(
110 credit_score=payload.credit_score,
111 monthly_income=payload.monthly_income,
112 existing_debt=payload.existing_debt,
113 employment_type=payload.employment_type,
114 dob_str=payload.date_of_birth,
115 amount_requested=payload.amount_requested,
116 term_months=payload.term_months,
117 )
118
119 # Decide auto-approval
120 if assessment.eligible and assessment.risk_score <= 30:
121 status = LoanStatus.APPROVED
122 amount_approved = assessment.max_loan_amount
123 interest_rate = assessment.suggested_interest_rate
124 rejection_reason = None
125 elif not assessment.eligible:
126 status = LoanStatus.REJECTED
127 amount_approved = None
128 interest_rate = None
129 rejection_reason = ”Auto-rejected: ” + ”; ”.join(
130 f for f in assessment.factors if ”excessive” in f.lower()
131 or ”poor” in f.lower() or ”unemployed” in f.lower()
132 or ”too high” in f.lower()
133 ) or ”Does not meet eligibility criteria”
134 else:
135 status = LoanStatus.UNDER_REVIEW
136 amount_approved = None
137 interest_rate = assessment.suggested_interest_rate
138 rejection_reason = None
139
140 loan = Loan(
141 borrower_id=borrower.id,
142 amount_requested=payload.amount_requested,
143 amount_approved=amount_approved,
144 interest_rate=interest_rate,
145 term_months=payload.term_months,
146 purpose=payload.purpose,
147 status=status,
148 risk_level=assessment.risk_level,
149 risk_score=assessment.risk_score,
150 rejection_reason=rejection_reason,
151 )
152 db.add(loan)
153 db.flush() # ensure loan.id is available
154
155 # Generate amortization for approved loans
156 if status == LoanStatus.APPROVED and amount_approved and interest_rate:
157 entries = generate_schedule(amount_approved, interest_rate, payload.term_months)
158 for entry in entries:
159 db.add(AmortizationEntry(
160 loan_id=loan.id,
161 month_number=entry.month_number,
162 payment_date=entry.payment_date,
163 beginning_balance=entry.beginning_balance,
164 monthly_payment=entry.monthly_payment,
165 principal=entry.principal,
166 interest=entry.interest,
167 ending_balance=entry.ending_balance,
168 ))
169
170 db.commit()
171 db.refresh(loan)
172 return loan
173
174
175# ---------------------------------------------------------------------------
176# LOANS – list / detail / update
177# ---------------------------------------------------------------------------
178
179@app.get(”/api/loans”, response_model=List[LoanOut])
180def list_loans(
181 status: LoanStatus | None = None,
182 skip: int = Query(0, ge=0),
183 limit: int = Query(50, ge=1, le=200),
184 db: Session = Depends(get_db),
185):
186 q = db.query(Loan)
187 if status:
188 q = q.filter(Loan.status == status)
189 return q.order_by(Loan.created_at.desc()).offset(skip).limit(limit).all()
190
191
192@app.get(”/api/loans/{loan_id}”, response_model=LoanOut)
193def get_loan(loan_id: str, db: Session = Depends(get_db)):
194 loan = db.query(Loan).filter(Loan.id == loan_id).first()
195 if not loan:
196 raise HTTPException(404, ”Loan not found”)
197 return loan
198
199
200@app.patch(”/api/loans/{loan_id}”, response_model=LoanOut)
201def update_loan_status(loan_id: str, payload: LoanStatusUpdate, db: Session = Depends(get_db)):
202 ”””Lender manually approves / rejects / disburses a loan.”””
203 loan = db.query(Loan).filter(Loan.id == loan_id).first()
204 if not loan:
205 raise HTTPException(404, ”Loan not found”)
206
207 loan.status = payload.status
208 if payload.amount_approved is not None:
209 loan.amount_approved = payload.amount_approved
210 if payload.interest_rate is not None:
211 loan.interest_rate = payload.interest_rate
212 if payload.rejection_reason is not None:
213 loan.rejection_reason = payload.rejection_reason
214
215 # Generate amortization on approval if not yet present
216 if payload.status == LoanStatus.APPROVED and loan.amount_approved and loan.interest_rate:
217 existing = db.query(AmortizationEntry).filter(AmortizationEntry.loan_id == loan_id).count()
218 if existing == 0:
219 entries = generate_schedule(loan.amount_approved, loan.interest_rate, loan.term_months)
220 for entry in entries:
221 db.add(AmortizationEntry(
222 loan_id=loan.id,
223 month_number=entry.month_number,
224 payment_date=entry.payment_date,
225 beginning_balance=entry.beginning_balance,
226 monthly_payment=entry.monthly_payment,
227 principal=entry.principal,
228 interest=entry.interest,
229 ending_balance=entry.ending_balance,
230 ))
231
232 db.commit()
233 db.refresh(loan)
234 return loan
235
236
237# ---------------------------------------------------------------------------
238# RISK ASSESSMENT (standalone – ”what-if” check)
239# ---------------------------------------------------------------------------
240
241@app.post(”/api/risk-assessment”, response_model=RiskAssessmentOut)
242def run_risk_assessment(payload: LoanApplicationCreate):
243 return assess_risk(
244 credit_score=payload.credit_score,
245 monthly_income=payload.monthly_income,
246 existing_debt=payload.existing_debt,
247 employment_type=payload.employment_type,
248 dob_str=payload.date_of_birth,
249 amount_requested=payload.amount_requested,
250 term_months=payload.term_months,
251 )
252
253
254# ---------------------------------------------------------------------------
255# AMORTIZATION SCHEDULE
256# ---------------------------------------------------------------------------
257
258@app.get(”/api/loans/{loan_id}/amortization”, response_model=List[AmortizationEntryOut])
259def get_amortization(loan_id: str, db: Session = Depends(get_db)):
260 loan = db.query(Loan).filter(Loan.id == loan_id).first()
261 if not loan:
262 raise HTTPException(404, ”Loan not found”)
263 return (
264 db.query(AmortizationEntry)
265 .filter(AmortizationEntry.loan_id == loan_id)
266 .order_by(AmortizationEntry.month_number)
267 .all()
268 )
269
270
271# ---------------------------------------------------------------------------
272# DOCUMENT UPLOAD
273# ---------------------------------------------------------------------------
274
275@app.post(”/api/loans/{loan_id}/documents”, response_model=DocumentOut, status_code=201)
276async def upload_document(loan_id: str, file: UploadFile = File(...), db: Session = Depends(get_db)):
277 loan = db.query(Loan).filter(Loan.id == loan_id).first()
278 if not loan:
279 raise HTTPException(404, ”Loan not found”)
280
281 ext = os.path.splitext(file.filename or ”file”)[1]
282 saved_name = f”{uuid.uuid4().hex}{ext}”
283 path = os.path.join(UPLOAD_DIR, saved_name)
284
285 contents = await file.read()
286 with open(path, ”wb”) as f:
287 f.write(contents)
288
289 doc = Document(
290 loan_id=loan_id,
291 filename=saved_name,
292 original_name=file.filename or ”unknown”,
293 content_type=file.content_type,
294 )
295 db.add(doc)
296 db.commit()
297 db.refresh(doc)
298 return doc
299
300
301@app.get(”/api/loans/{loan_id}/documents”, response_model=List[DocumentOut])
302def list_documents(loan_id: str, db: Session = Depends(get_db)):
303 return db.query(Document).filter(Document.loan_id == loan_id).all()
304
305
306# ---------------------------------------------------------------------------
307# BORROWERS
308# ---------------------------------------------------------------------------
309
310@app.get(”/api/borrowers”, response_model=List[BorrowerOut])
311def list_borrowers(db: Session = Depends(get_db)):
312 return db.query(Borrower).order_by(Borrower.created_at.desc()).all()
313
314
315@app.get(”/api/borrowers/{borrower_id}”, response_model=BorrowerOut)
316def get_borrower(borrower_id: str, db: Session = Depends(get_db)):
317 b = db.query(Borrower).filter(Borrower.id == borrower_id).first()
318 if not b:
319 raise HTTPException(404, ”Borrower not found”)
320 return b
321
322
323# ---------------------------------------------------------------------------
324# DASHBOARD / ANALYTICS
325# ---------------------------------------------------------------------------
326
327@app.get(”/api/dashboard”, response_model=DashboardStats)
328def dashboard(db: Session = Depends(get_db)):
329 total = db.query(func.count(Loan.id)).scalar() or 0
330 pending = db.query(func.count(Loan.id)).filter(Loan.status == LoanStatus.PENDING).scalar() or 0
331 under_review = db.query(func.count(Loan.id)).filter(Loan.status == LoanStatus.UNDER_REVIEW).scalar() or 0
332 approved = db.query(func.count(Loan.id)).filter(
333 Loan.status.in_([LoanStatus.APPROVED, LoanStatus.DISBURSED])
334 ).scalar() or 0
335 rejected = db.query(func.count(Loan.id)).filter(Loan.status == LoanStatus.REJECTED).scalar() or 0
336
337 total_disbursed = (
338 db.query(func.coalesce(func.sum(Loan.amount_approved), 0))
339 .filter(Loan.status == LoanStatus.DISBURSED)
340 .scalar()
341 )
342 avg_amount = db.query(func.coalesce(func.avg(Loan.amount_requested), 0)).scalar()
343 avg_risk = db.query(func.coalesce(func.avg(Loan.risk_score), 0)).scalar()
344
345 return DashboardStats(
346 total_applications=total,
347 pending=pending + under_review,
348 approved=approved,
349 rejected=rejected,
350 total_disbursed_amount=round(float(total_disbursed), 2),
351 average_loan_amount=round(float(avg_amount), 2),
352 average_risk_score=round(float(avg_risk), 2),
353 )
Autonomous agents
Hand off the task. The agent runs the whole loop.
- Plans the approach and shows you the path before it touches anything
- Works through the task end to end without step-by-step prompting
- Catches failures and revises before handing back
- Returns a reviewable summary of what changed and why
Reading filesWaiting
Plan readyWaiting
Files editedWaiting
Checks runningWaiting
Preparing summaryWaiting
Ready for reviewWaiting
corepass-code.html+3 -11
<a href="#" class="btn btn-nav"><span class="btn-text">Try Remy</span></a>
<a href="#" class="btn btn-nav" data-download><span class="btn-text">Download for macOS</span></a>
— edit 2 —
<a href="#" class="btn btn-lg btn-white"><span>Download for macOS</span></a>
<a href="#" class="btn btn-lg btn-ghost btn-arrow-after">
Real implementation
Real code in your repo, not generated screens.
- Produces working features, not clickable mockups
- Edits your actual codebase, and the result runs
- Refactors across files with behavior intact
- Lands with test coverage, so the change is verified, not assumed
Work across desktop, CLI, and cloud.
Start in the desktop app, run tasks from the terminal, or let longer work continue in the cloud.
CLI
Start tasks, inspect progress, and run agents from your terminal.
Context per task
Focused search loads only the files each task needs.
Without focused searchRemy
Context loaded
68% less context
Startup time
2.2× faster start
Task cost
62% lower cost
›Quick Search finds the right files before the agent starts.
Lower cost per task
- Pay for the context the task needs, not the whole repo
- Skip the broad scan and start editing sooner
- Get more done per dollar, not per prompt
- Predictable cost even as the codebase grows
Instant revert
- Let the agent try larger changes, undo is always one click
- See every change before it becomes permanent
- Roll back a full run to the exact state you started from
- Run more experiments without touching your working branch
“Build a pricing page from an existing design system”
6 files changed
Checks
pricing.html+148 -32
<section class="pricing-hero">
<h1>Plans that scale with you</h1>
<section class="placeholder">
Working branch restored
Describe a task or ask a question
The right model for each task
Match the model's power to the work, not to the price tag.
- A lightweight model for small edits and quick fixes
- A stronger model for refactors and long runs
- Dynamic picks for you when you would rather not choose
- Search every model and switch in one click
Start building with Remy.
Turn ideas into working software with an AI coding agent that searches, builds, tests, and keeps you in control.
›corepass code "Build the usage dashboard, run checks, and prepare the changes"
