Skip to content

SMPLINVST #393

@anateresaaleman1-crypto

Description

@anateresaaleman1-crypto

"""
SMPLINVST — Investing made clear, opportunities made simple.

This single Flask app includes:

  • Profile personalization
  • Brands → Suppliers
  • Compare view (with tickers wired for prices when POLYGON_API_KEY is set)
  • Watchlist
  • AI Coach (OpenAI if OPENAI_API_KEY set; otherwise calm default)
  • Humanized Tailwind UI
  • Integrated React single-file demo at /react (X-Ray, Compare, Education, Coach)

Run:
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install flask requests python-dotenv

(optional) pip install openai

python smplinvst_app.py

Open:
http://127.0.0.1:5000 (Flask UI)
http://127.0.0.1:5000/react (React single-file demo)
"""

import os
import sqlite3
from contextlib import closing
from typing import Optional

import requests
from flask import Flask, request, redirect, url_for, render_template_string, flash
from jinja2 import DictLoader
from dotenv import load_dotenv

--- OpenAI is optional; fail-safe if not installed or not configured ---

try:
import openai # type: ignore
_OPENAI_IMPORTED = True
except Exception:
openai = None # type: ignore
_OPENAI_IMPORTED = False

-------------------------

Config

-------------------------

APP_NAME = "SMPLINVST"
DB_PATH = "smplinvst.db"

load_dotenv()
POLYGON_API_KEY = os.getenv("POLYGON_API_KEY")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")

if _OPENAI_IMPORTED and OPENAI_API_KEY:
try:
# Old SDK interface for broad compatibility
openai.api_key = OPENAI_API_KEY # type: ignore[attr-defined]
except Exception:
pass

app = Flask(name)
app.secret_key = os.getenv("FLASK_SECRET_KEY", "dev-secret-change-me") # replace for prod

-------------------------

Domain Data (tickers included)

-------------------------

BRANDS = {
"Apple": {
"ticker": "AAPL",
"summary": "Consumer tech giant. The hidden opportunity often lies in its suppliers.",
"suppliers": [
{"name": "TSMC", "ticker": "TSM", "why": "Advanced chips powering Apple devices."},
{"name": "Foxconn", "ticker": None, "why": "Assembles iPhones and other hardware."},
{"name": "Corning", "ticker": "GLW", "why": "Specialty glass for Apple products."},
],
},
"Nike": {
"ticker": "NKE",
"summary": "Global apparel brand. Margins hinge on suppliers’ scale and efficiency.",
"suppliers": [
{"name": "Sheng Hung", "ticker": None, "why": "Textiles and performance fabrics."},
{"name": "Fulgent Sun", "ticker": None, "why": "Large-scale footwear manufacturing."},
{"name": "Avery Dennison", "ticker": "AVY", "why": "Labels and supply-chain tech."},
],
},
"Tesla": {
"ticker": "TSLA",
"summary": "EV pioneer. Battery ecosystem is the real story.",
"suppliers": [
{"name": "Panasonic", "ticker": "PCRFY", "why": "Battery cells for Tesla models."},
{"name": "CATL", "ticker": None, "why": "LFP battery supplier in key regions."},
{"name": "NVIDIA", "ticker": "NVDA", "why": "Compute for AI/Autopilot systems."},
],
},
}

-------------------------

Database

-------------------------

SCHEMA = """
CREATE TABLE IF NOT EXISTS profile (
id INTEGER PRIMARY KEY CHECK (id=1),
first_name TEXT,
life_goal TEXT,
risk_profile TEXT
);
CREATE TABLE IF NOT EXISTS watchlist (
id INTEGER PRIMARY KEY AUTOINCREMENT,
item_type TEXT CHECK(item_type IN ('brand','supplier')) NOT NULL,
name TEXT NOT NULL
);
"""

def get_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn

def init_db():
with closing(get_db()) as db:
db.executescript(SCHEMA)
db.commit()

@app.before_first_request
def _init():
init_db()

-------------------------

Helpers

-------------------------

def get_price(ticker: Optional[str]) -> str:
"""Fetch last trade price from Polygon if configured; otherwise '—'."""
if not ticker or not POLYGON_API_KEY:
return "—"
try:
r = requests.get(
f"https://api.polygon.io/v2/last/trade/{ticker}",
params={"apiKey": POLYGON_API_KEY},
timeout=6,
)
if r.ok:
js = r.json() or {}
price = (js.get("results") or {}).get("p")
return f"${price:.2f}" if isinstance(price, (float, int)) else "N/A"
except Exception:
return "N/A"
return "N/A"

def ai_coach(note: Optional[str] = None) -> str:
"""Calm, encouraging coach. Uses OpenAI if key present & SDK available; else a default tip."""
if not (_OPENAI_IMPORTED and OPENAI_API_KEY):
return "Coach’s note: Tiny steps → big outcomes. Stay consistent, stay calm."
# Try legacy ChatCompletion first for compatibility
try:
# type: ignore[union-attr]
resp = openai.ChatCompletion.create( # noqa
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a calm, encouraging investing coach for SMPLINVST."},
{"role": "user", "content": note or "Give a short, plain-language investing encouragement."},
],
max_tokens=120,
temperature=0.6,
)
return resp["choices"][0]["message"]["content"].strip()
except Exception:
# Final fallback — static tip
return "Coach unavailable right now. Keep the process small and steady."

-------------------------

HTML Templates (Flask UI)

-------------------------

BASE_HTML = """
<!doctype html>

<title>{{ title }}</title> <script src="https://cdn.tailwindcss.com"></script>

{{ app_name }}

🏠 Home 📈 Compare 💡 Coach 🧪 React Demo Reset {% with messages = get_flashed_messages() %} {% if messages %} {% for m in messages %}
{{ m }}
{% endfor %} {% endif %} {% endwith %} {% block content %}{% endblock %}
SMPLINVST · Investing made clear, opportunities made simple. """

HOME_HTML = """
{% extends 'base.html' %}
{% block content %}
{% if not profile %}

Personalize your journey

ConservativeBalancedAggressive Save {% else %}

Hey {{ profile['first_name'] }}, your goal is: {{ profile['life_goal'] }}

Risk: {{ profile['risk_profile'] }}

Brands → Suppliers

{% for brand, info in brands.items() %}

{{ brand }}

+ Watch

{{ info.summary }}

Stock: {{ get_price(info.ticker) }}

    {% for s in info.suppliers %}
  • {{ s.name }} + Watch
  • {% endfor %}
{% endfor %}

⭐ Your Watchlist

{% if not watchlist %}

Nothing yet. Add brands or suppliers that resonate with you.

{% else %}
    {% for row in watchlist %}
  • {{ row['name'] }} ({{ row['item_type'] }}) Remove
  • {% endfor %}
{% endif %} {% endif %} {% endblock %} """

COMPARE_HTML = """
{% extends 'base.html' %}
{% block content %}

Compare

{% for b in brands.keys() %} {{ b }} {% endfor %} Compare {% if picked %}

{{ picked }}

{{ brands[picked].summary }}

Stock: {{ get_price(brands[picked].ticker) }}

{% for s in brands[picked].suppliers %}

{{ s.name }}

{{ s.why }}

Stock: {{ get_price(s.ticker) }}

{% endfor %}

{{ coach_note }}

{% endif %} {% endblock %} """

COACH_HTML = """
{% extends 'base.html' %}
{% block content %}

💡 AI Coach

Ask {% if answer %}

{{ answer }}

{% else %}

{{ default_note }}

{% endif %} {% endblock %} """

-------------------------

React Demo Page (/react)

-------------------------

REACT_HTML = """
<!doctype html>

<title>SMPLINVST — React Demo</title> <style>html,body,#root{height:100%}body{margin:0;background:#000;color:#fff;font-family:system-ui,-apple-system,Segoe UI,Roboto,Inter,sans-serif}</style> <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> <script crossorigin src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel" data-presets="react"> /* @jsxRuntime classic */ /* @jsx React.createElement */

// React resolver
let ReactLocal;
try { ReactLocal = require('react'); } catch { ReactLocal = (globalThis && (globalThis.React || globalThis.preactCompat || globalThis.preact)) || null; }
if (!ReactLocal) { throw new Error('SMPLINVST: React not available. Provide window.React or enable require("react").'); }
const React = ReactLocal;

// --- Data ---
const DB = {
brands: {
Apple: {
ticker: 'AAPL',
thesis: 'Brand scale + ecosystem lock-in. Supplier edge: concentrated tech margin capture (chips/displays).',
suppliers: [
{ name: 'TSMC', ticker: 'TSM', role: 'Chips / Foundry', risk: 3 },
{ name: 'Broadcom', ticker: 'AVGO', role: 'Wireless Chips', risk: 2 },
{ name: 'LG Display', ticker: 'LPL', role: 'Displays', risk: 4 }
]
},
Nike: {
ticker: 'NKE',
thesis: 'Global brand + distribution. Supplier angle: inputs & scale logistics.',
suppliers: [
{ name: '3M', ticker: 'MMM', role: 'Materials', risk: 2 },
{ name: 'Amcor', ticker: 'AMCR', role: 'Packaging', risk: 2 },
{ name: 'VF Corp', ticker: 'VFC', role: 'Materials/Apparel', risk: 4 }
]
},
Starbucks: {
ticker: 'SBUX',
thesis: 'Consumer habit + footprint. Supplier angle: distribution & packaging.',
suppliers: [
{ name: 'PepsiCo', ticker: 'PEP', role: 'Distribution', risk: 2 },
{ name: 'Nestlé', ticker: 'NSRGY', role: 'Canned coffee partner', risk: 2 },
{ name: 'Amcor', ticker: 'AMCR', role: 'Packaging', risk: 3 }
]
}
},
prices: {}
};

(function seedPrices(){
function rng(seed){ let s=seed; return ()=> (s = (s1664525+1013904223)>>>0)/4294967296; }
function makeSeries(seed){
const r=rng(seed), arr=[]; let p=100+50
r();
for(let i=0;i<30;i++){ const drift=(r()-0.5)*2; p=Math.max(5, p+drift); arr.push(+p.toFixed(2)); }
return arr;
}
const tickers = ['AAPL','TSM','AVGO','LPL','NKE','MMM','AMCR','VFC','SBUX','PEP','NSRGY'];
tickers.forEach((t,i)=> DB.prices[t]=makeSeries(12345+i));
})();

// --- Utils ---
function safeDecode(v){ try { return decodeURIComponent(v ?? ''); } catch { return String(v ?? ''); } }
function clamp(n,min,max){ return Math.min(max, Math.max(min,n)); }
function percent(a,b){ if(!a||!b) return 0; return ((a-b)/b)100; }
function mean(xs){ return xs.reduce((s,x)=>s+x,0)/xs.length; }
function stdev(xs){ const m=mean(xs); const v=mean(xs.map(x=>(x-m)
(x-m))); return Math.sqrt(v); }
function returns(xs){ const r=[]; for(let i=1;i<xs.length;i++){ r.push((xs[i]-xs[i-1])/xs[i-1]); } return r; }
function metricsFor(ticker){
const s = DB.prices[ticker] || [];
if (s.length<2) return { last:null, first:null, changePct:null, vol:null };
const last=s[s.length-1], first=s[0];
const r = returns(s);
return { last, first, changePct: percent(last, first), vol: stdev(r)Math.sqrt(252) };
}
function sparkpath(xs,width=160,height=40){
if(!xs||!xs.length) return '';
const min=Math.min(...xs), max=Math.max(...xs); const span=max-min||1;
const step=width/(xs.length-1); let d='';
for(let i=0;i<xs.length;i++){
const x=i
step; const y=height - ((xs[i]-min)/span)*height;
d += (i? ' L':'M')+x.toFixed(2)+' '+y.toFixed(2);
}
return d;
}
function persist(key, val){ try{ localStorage.setItem(key, JSON.stringify(val)); }catch{} }
function restore(key, dflt){ try{ const v=localStorage.getItem(key); return v? JSON.parse(v): dflt; }catch{ return dflt; } }
function parseHashPath(){ const raw=(typeof window!=='undefined'?window.location.hash:'')||'#/' ; const path=raw.startsWith('#')?raw.slice(1):raw; const cleaned=path.trim()||'/'; const parts=cleaned.replace(/^/+|/+$/g,'').split('/'); if(parts.length===1&&(parts[0]===''||parts[0]==='/')) return []; return parts.map(safeDecode); }
function matchRoute(pattern, parts){ const out={ok:false, params:{}}; if(!Array.isArray(parts)) return out; if(pattern.length!==parts.length) return out; const params={}; for(let i=0;i<pattern.length;i++){ const seg=pattern[i], val=parts[i]; if(seg.startsWith(':')) params[seg.slice(1)]=val; else if(seg!==val) return out; } return {ok:true, params}; }
function navigate(hash){ if(typeof window==='undefined') return; const t=hash.startsWith('#')?hash:'#'+hash; if(window.location.hash===t) return; window.location.hash=t; }

// --- Intent coach (no network) ---
function coachAnswer(q){
const s = (q||'').toLowerCase();
if(/supplier|supply|behind the brand/.test(s)) return 'Suppliers make critical parts behind big brands. Owning suppliers can capture value the logo doesn’t — esp. chips.';
if(/risk|vol/.test(s)) return 'Rule: brands ≈ steadier cashflows; suppliers ≈ higher cyclicality. Match to your risk slider and diversify.';
if(/apple|tsm|avgo/.test(s)){
const a=metricsFor('AAPL'), t=metricsFor('TSM');
return AAPL ${a.changePct? a.changePct.toFixed(1)+'%':''} vs TSM ${t.changePct? t.changePct.toFixed(1)+'%':''} (30d). Brands defend demand; foundries monetize scale.;
}
if(/diversif/.test(s)) return 'Spread positions across brand and 2–3 suppliers. Avoid single-point failures. Rebalance quarterly.';
return 'Ask about suppliers vs brands, risk, or Apple/Nike/Starbucks.';
}

// --- Styles ---
const css = {
page: { minHeight:'100vh', background:'#000', color:'#fff', fontFamily:'system-ui,-apple-system,Segoe UI,Roboto,Inter,sans-serif' },
header: { padding:'12px 16px', borderBottom:'1px solid rgba(255,255,255,0.12)', display:'flex', justifyContent:'space-between', alignItems:'center' },
navlink: { color:'#bbb', textDecoration:'none', marginLeft:12 },
button: { padding:'10px 14px', background:'#059669', borderRadius:10, color:'#fff', textDecoration:'none', display:'inline-block' },
card: { border:'1px solid rgba(255,255,255,0.12)', background:'rgba(255,255,255,0.04)', borderRadius:16, padding:16 },
muted: { color:'#bbb' }
};

// --- UI bits ---
function Badge({label, tone}){ const bg = tone==='good'? '#064e3b' : tone==='warn'? '#7c2d12' : '#1f2937';
return <span style={{ background:bg, color:'#fff', padding:'4px 8px', borderRadius:999, fontSize:12, marginLeft:8 }}>{label};
}
function Metric({label, value}){ return (<div style={{ display:'flex', justifyContent:'space-between', marginTop:6 }}><span style={{ color:'#aaa' }}>{label}{value}); }
function Spark({series, width=160, height=40}){ const d=sparkpath(series,width,height); const up=(series?.[series.length-1]||0)>=(series?.[0]||0); const stroke=up?'#10b981':'#ef4444'; return (<svg width={width} height={height} style={{ display:'block', width:'100%' }}>); }

// --- Pages ---
function HomePage(){ return (

See Beyond the Logo

SMPLINVST reveals the supplier layer — the backbone behind logos.

);}

function SupplierChip({s, brand}){ return (
<a href={#/compare/${encodeURIComponent(brand)}/${encodeURIComponent(s.ticker)}}
onClick={(e)=>{e.preventDefault();navigate(#/compare/${encodeURIComponent(brand)}/${encodeURIComponent(s.ticker)});}}
style={{ display:'inline-block', margin:6, padding:'8px 12px', borderRadius:12, border:'1px solid rgba(255,255,255,0.16)', background:'rgba(255,255,255,0.04)', textDecoration:'none', color:'#fff' }}>
<div style={{ fontWeight:600 }}>{s.name} <span style={{ color:'#9ca3af' }}>({s.ticker})
<div style={{ fontSize:12, color:'#9ca3af' }}>{s.role}

);}

function XRayPage({ brand }){ const b=DB.brands[brand]; if(!b) return (

Brand not found

Try Apple, Nike, or Starbucks.

); return (
{brand}
Ticker: {b.ticker}
Cracking the logo…
Suppliers that power {brand}
{b.suppliers.map((s)=> )}

{b.thesis}

);}

function CompareCard({ title, ticker, risk, personalizedRisk }){ const s=DB.prices[ticker]||[]; const m=metricsFor(ticker); const fit=personalizedRisk!=null?(personalizedRisk>=(risk||3)?'Fit':'Cautious'):null; return (

{title} ({ticker})
{fit && }
Last{m.last?`$${m.last.toFixed(2)}`:'—'}
30d Change{m.changePct!=null?`${m.changePct.toFixed(1)}%`:'—'}
Vol (ann.){m.vol!=null?`${(m.vol*100).toFixed(1)}%`:'—'}
);}

function ComparePage({ brand, supplier }){ const b=DB.brands[brand]; const suppliers=b?.suppliers||[]; const mapped=suppliers.find(x=>x.ticker===supplier); const riskPref=restore('smplinvst:risk',3);
if(!b) return (<section style={{ padding:20 }}><h2 style={{ fontSize:24, fontWeight:800 }}>Brand not found

Try Apple, Nike, or Starbucks.

);
if(!mapped) return (
<section style={{ padding:20, maxWidth:1000, margin:'0 auto' }}>
<h2 style={{ fontSize:24, fontWeight:800 }}>{brand} vs {supplier}

Support for this supplier isn’t available yet — it’s being added.


<div style={{ marginTop:12 }}>Try one of the mapped suppliers:
<div style={{ marginTop:8 }}>{suppliers.map((s)=> )}

);
const left={ title:${brand} (Brand), ticker:b.ticker, risk:2 }, right={ title:${mapped.name} (Supplier), ticker:mapped.ticker, risk:mapped.risk };
return (
<section style={{ padding:20, maxWidth:1000, margin:'0 auto' }}>
<h2 style={{ fontSize:24, fontWeight:800, marginBottom:12 }}>{brand}: Brand vs Supplier
<div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:12 }}>



<div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:12, marginTop:12 }}>
<div style={{ fontWeight:700, marginBottom:6 }}>Why brand?
<ul style={{ margin:0, paddingLeft:18, color:'#d1d5db' }}>
  • Demand resilience via ecosystem & distribution
  • Lower operational cyclicality
  • Dividends/buybacks more common

  • <div style={{ fontWeight:700, marginBottom:6 }}>Why supplier?
    <ul style={{ margin:0, paddingLeft:18, color:'#d1d5db' }}>
  • High-margin components capture more value per unit
  • Beneficiary of multiple brands’ growth
  • Earlier cycle exposure → potentially higher beta


  • <div style={{ ...css.muted, marginTop:12 }}>*Data is illustrative (synthetic 30d). Wire Polygon later for live quotes.

    );
    }

    function EducationPage(){ return (

    Education

    Suppliers vs Brands 101

    A supplier sells critical parts to many brands. That diversification can smooth single-brand risk while retaining margin exposure.

    Risk & Fit

    Higher tech concentration → higher volatility. Use the risk slider to match positions to your tolerance.

    );}

    // Sidebar coach & risk
    class Coach extends React.Component{ constructor(p){ super(p); this.state={ open:true, q:'Explain suppliers vs brands', a:'' }; }
    ask(){ const a=coachAnswer(this.state.q); this.setState({a}); }
    render(){ if(!this.state.open) return (<div style={{ position:'fixed', right:16, bottom:16 }}><button onClick={()=>this.setState({open:true})} style={{ ...css.button, background:'#1f2937' }}>Coach);
    return (<div style={{ position:'fixed', right:16, bottom:16, width:320 }}>
    <div style={{ ...css.card }}>
    <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:6 }}>
    <div style={{ fontWeight:700 }}>Coach
    <button onClick={()=>this.setState({open:false})} style={{ ...css.button, background:'#374151', padding:'6px 10px' }}>Hide

    <textarea value={this.state.q} onChange={e=>this.setState({q:e.target.value})}
    style={{ width:'100%', height:72, background:'rgba(255,255,255,0.06)', color:'#fff', border:'1px solid rgba(255,255,255,0.18)', borderRadius:8, padding:8 }} />
    <div style={{ display:'flex', justifyContent:'flex-end', marginTop:8 }}>
    <button onClick={()=>this.ask()} style={{ ...css.button }}>Ask

    {this.state.a && <div style={{ marginTop:8, color:'#e5e7eb', whiteSpace:'pre-wrap' }}>{this.state.a}}

    );
    }
    }

    class RiskControl extends React.Component{ constructor(p){ super(p); this.state={ val: restore('smplinvst:risk', 3) }; }
    set(v){ const nv=Math.min(5, Math.max(1, Number(v))); this.setState({val:nv}); persist('smplinvst:risk', nv); if(this.props.onChange) this.props.onChange(nv); }
    render(){ return (
    <div style={{ ...css.card, display:'flex', alignItems:'center', gap:12 }}>
    <div style={{ fontWeight:700 }}>Risk preference
    <input type="range" min="1" max="5" value={this.state.val} onChange={e=>this.set(e.target.value)} />
    <div style={{ color:'#d1d5db' }}>{this.state.val}

    );}
    }

    // Layout & App
    function Header(){ return (

    SMPLINVST
    {e.preventDefault();navigate('#/');}} style={css.navlink}>Home {e.preventDefault();navigate('#/xray/Apple');}} style={css.navlink}>Explore {e.preventDefault();navigate('#/edu');}} style={css.navlink}>Education );}

    class App extends React.Component{
    constructor(p){ super(p); this.state={ parts: parseHashPath(), risk: restore('smplinvst:risk',3) }; this.onHash=this.onHash.bind(this); }
    componentDidMount(){ if(typeof window!=='undefined' && !window.location.hash) navigate('#/'); window.addEventListener('hashchange', this.onHash); }
    componentWillUnmount(){ window.removeEventListener('hashchange', this.onHash); }
    onHash(){ this.setState({ parts: parseHashPath() }); }
    render(){ const { parts } = this.state; let content=null;
    if(parts.length===0){ content=; }
    if(!content){ const m=matchRoute(['xray',':brand'], parts); if(m.ok) content=; }
    if(!content){ const m=matchRoute(['compare',':brand',':supplier'], parts); if(m.ok) content=; }
    if(!content){ const m=matchRoute(['edu'], parts); if(m.ok) content=; }
    if(!content){ content=(<section style={{ padding:20 }}><h2 style={{ fontSize:22, fontWeight:800 }}>Not Found

    Go <a href="#/" onClick={(e)=>{e.preventDefault();navigate('#/');}} style={{ color:'#9ca3af' }}>Home.

    ); }
    return (
    <div style={{ padding:16, maxWidth:1100, margin:'0 auto', display:'grid', gap:12 }}><RiskControl onChange={(v)=>this.setState({risk:v})} />{content}
    );
    }
    }

    // Mount
    const root = ReactDOM.createRoot(document.getElementById('root'));
    root.render();
    </script>

    """

    -------------------------

    Routes (Flask UI)

    -------------------------

    @app.route("/")
    def home():
    with closing(get_db()) as db:
    profile = db.execute("SELECT * FROM profile WHERE id=1").fetchone()
    watchlist = db.execute("SELECT * FROM watchlist ORDER BY id DESC").fetchall()
    return render_template_string(
    HOME_HTML,
    app_name=APP_NAME,
    title="Home",
    profile=profile,
    brands=BRANDS,
    watchlist=watchlist,
    get_price=get_price,
    )

    @app.route("/set_profile", methods=["POST"])
    def set_profile():
    first_name = (request.form.get("first_name") or "").strip()
    life_goal = (request.form.get("life_goal") or "").strip()
    risk_profile = (request.form.get("risk_profile") or "Balanced").strip()
    if not first_name or not life_goal:
    flash("Please complete your name and goal.")
    return redirect(url_for("home"))

    with closing(get_db()) as db:
        db.execute("DELETE FROM profile")
        db.execute(
            "INSERT INTO profile(id,first_name,life_goal,risk_profile) VALUES (1,?,?,?)",
            (first_name, life_goal, risk_profile),
        )
        db.commit()
    flash("Profile saved. Welcome aboard!")
    return redirect(url_for("home"))
    

    @app.route("/compare")
    def compare():
    brand = request.args.get("brand")
    picked = brand if brand in BRANDS else None
    coach_note = ai_coach("Give a short, friendly note about comparing a brand vs its suppliers.") if picked else ""
    return render_template_string(
    COMPARE_HTML,
    app_name=APP_NAME,
    title="Compare",
    brands=BRANDS,
    picked=picked,
    get_price=get_price,
    coach_note=coach_note,
    )

    @app.route("/watchlist/add", methods=["POST"])
    def add_watchlist():
    item_type = request.form.get("item_type")
    name = (request.form.get("name") or "").strip()
    if item_type not in {"brand", "supplier"} or not name:
    flash("Invalid item.")
    return redirect(request.referrer or url_for("home"))
    with closing(get_db()) as db:
    db.execute("INSERT INTO watchlist(item_type,name) VALUES(?,?)", (item_type, name))
    db.commit()
    flash("Added to watchlist.")
    return redirect(request.referrer or url_for("home"))

    @app.route("/watchlist/remove", methods=["POST"])
    def remove_watchlist():
    _id = request.form.get("id")
    with closing(get_db()) as db:
    db.execute("DELETE FROM watchlist WHERE id=?", (_id,))
    db.commit()
    flash("Removed from watchlist.")
    return redirect(request.referrer or url_for("home"))

    @app.route("/coach", methods=["GET", "POST"])
    def coach():
    answer = None
    if request.method == "POST":
    q = request.form.get("question", "")
    answer = ai_coach(q)
    return render_template_string(
    COACH_HTML,
    app_name=APP_NAME,
    title="Coach",
    answer=answer,
    default_note=ai_coach(),
    )

    @app.route("/reset")
    def reset():
    if os.path.exists(DB_PATH):
    os.remove(DB_PATH)
    init_db()
    flash("Reset complete.")
    return redirect(url_for("home"))

    React demo route

    @app.route("/react")
    def react_demo():
    return REACT_HTML

    -------------------------

    Template Loader

    -------------------------

    app.jinja_loader = DictLoader({
    "base.html": BASE_HTML,
    "home.html": HOME_HTML,
    "compare.html": COMPARE_HTML,
    "coach.html": COACH_HTML,
    })

    -------------------------

    Run

    -------------------------

    if name == "main":
    # Set host="0.0.0.0" if you want to access from other devices on your LAN
    app.run(host="127.0.0.1", port=int(os.getenv("PORT", "5000")), debug=True)

    Metadata

    Metadata

    Assignees

    No one assigned

      Labels

      No labels
      No labels

      Type

      No type

      Projects

      No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions