mathiassandnes Claude Opus 4.5 commited on
Commit
bba0cac
·
1 Parent(s): bcf0c50

Add C-Level strategy view draft and auto-deploy workflow

Browse files

- Add strategy_v2.py: Simplified C-level dashboard with:
- 4 input sliders (portfolio, lease price, mfg cost, cycles)
- Break-even hero metric at portfolio level
- Total portfolio profit display
- Unit economics (revenue/cost per product & portfolio)
- Circular vs Linear comparison cards
- Cost/Revenue trajectory chart by cycles
- At Scale section with customer base projection
- 5-year cumulative profit chart

- Update deploy-hf.md skill for automatic deployment
- Update CLAUDE.md with HF deployment instructions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Files changed (3) hide show
  1. .claude/skills/deploy-hf.md +30 -30
  2. CLAUDE.md +19 -6
  3. strategy_v2.py +449 -0
.claude/skills/deploy-hf.md CHANGED
@@ -5,51 +5,51 @@ Deploy the simulator to Hugging Face Space.
5
  ## Usage
6
 
7
  ```
8
- /deploy-hf
9
  ```
10
 
11
  ## Instructions
12
 
13
- 1. **Ensure you're on main branch:**
14
- ```bash
15
- git checkout main
16
- ```
17
 
18
- 2. **Verify all tests pass:**
19
- ```bash
20
- pytest tests/ -v
21
- ```
22
 
23
- 3. **Push to HF Space:**
24
  ```bash
25
- git push hf main --force
26
  ```
27
 
28
- If `hf` remote not configured:
29
- ```bash
30
- git remote add hf https://huggingface.co/spaces/Liisdotcom/simulator
31
- ```
32
 
33
- ## Prerequisites
 
 
 
 
 
 
34
 
35
- - Must be on `main` branch (not `dev`)
36
- - All tests must pass
37
- - HF remote must be configured with write access
38
 
39
- ## Via GitHub Actions (Recommended)
40
 
41
- 1. Go to GitHub repo → Actions → "Deploy to HF Space"
42
- 2. Click "Run workflow"
43
- 3. Type "deploy" to confirm
44
- 4. Wait for deployment to complete
 
45
 
46
- This method:
47
- - Runs tests before deploying
48
- - Uses stored HF_TOKEN secret
49
- - Creates deployment summary
50
 
51
  ## After Deployment
52
 
53
- Verify at: https://huggingface.co/spaces/Liisdotcom/simulator
54
 
55
- The Space will rebuild automatically (1-2 minutes).
 
5
  ## Usage
6
 
7
  ```
8
+ /deploy-hf [commit message]
9
  ```
10
 
11
  ## Instructions
12
 
13
+ This skill automatically commits and deploys changes to Hugging Face Spaces.
 
 
 
14
 
15
+ 1. **Stage and commit changes:**
16
+ - Stage modified files (app.py, strategy_v2.py, etc.)
17
+ - Generate commit message from changes if not provided
18
+ - Commit with Co-Authored-By tag
19
 
20
+ 2. **Push to HF Space:**
21
  ```bash
22
+ git push hf main
23
  ```
24
 
25
+ 3. **Confirm deployment:**
26
+ - Output the HF Spaces URL
27
+ - Note that rebuild takes 1-2 minutes
 
28
 
29
+ ## Auto-Deploy Workflow
30
+
31
+ When iterating on this project, ALWAYS deploy after making changes:
32
+
33
+ ```bash
34
+ # 1. Add changed files
35
+ git add app.py strategy_v2.py # or specific files changed
36
 
37
+ # 2. Commit with descriptive message
38
+ git commit -m "Description of changes
 
39
 
40
+ Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"
41
 
42
+ # 3. Push to HF
43
+ git push hf main
44
+ ```
45
+
46
+ ## Prerequisites
47
 
48
+ - HF remote configured: `git remote add hf https://huggingface.co/spaces/Liisdotcom/simulator`
49
+ - Write access to the Space
 
 
50
 
51
  ## After Deployment
52
 
53
+ Live at: https://huggingface.co/spaces/Liisdotcom/simulator
54
 
55
+ The Space rebuilds automatically (1-2 minutes).
CLAUDE.md CHANGED
@@ -129,15 +129,28 @@ revenue_per_customer = lease_price * lease_term
129
 
130
  ## Deployment
131
 
132
- **Streamlit Cloud (recommended):**
133
- 1. Go to share.streamlit.io
134
- 2. Connect GitHub repo (must be public or grant org access)
135
- 3. Deploy `app.py` from `main` branch
136
 
137
- **Local:**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  ```bash
139
  pip install -r requirements.txt
140
- streamlit run app.py
 
141
  ```
142
 
143
  ## Tech Stack
 
129
 
130
  ## Deployment
131
 
132
+ ### Hugging Face Spaces (Primary)
 
 
 
133
 
134
+ **IMPORTANT: Always deploy after each iteration!**
135
+
136
+ After making any changes, commit and push to HF:
137
+ ```bash
138
+ git add <changed-files>
139
+ git commit -m "Description
140
+
141
+ Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"
142
+ git push hf main
143
+ ```
144
+
145
+ Live at: https://huggingface.co/spaces/Liisdotcom/simulator
146
+
147
+ Use `/deploy-hf` skill or manually run the commands above.
148
+
149
+ ### Local Development
150
  ```bash
151
  pip install -r requirements.txt
152
+ streamlit run app.py # Main dashboard
153
+ streamlit run strategy_v2.py # C-Level strategy view (draft)
154
  ```
155
 
156
  ## Tech Stack
strategy_v2.py ADDED
@@ -0,0 +1,449 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LiiS Circular Economy Simulator - C-Level View (Draft)
3
+ Simplified executive dashboard focused on portfolio-level economics.
4
+
5
+ Run with: streamlit run strategy_v2.py
6
+ """
7
+
8
+ import streamlit as st
9
+ import plotly.graph_objects as go
10
+ import pandas as pd
11
+ import plotly.io as pio
12
+
13
+ # Set Plotly to dark theme
14
+ pio.templates.default = "plotly_dark"
15
+
16
+ # Page config
17
+ st.set_page_config(
18
+ page_title="LiiS Strategy",
19
+ page_icon="💼",
20
+ layout="wide",
21
+ )
22
+
23
+ # Custom CSS
24
+ st.markdown("""
25
+ <style>
26
+ .main-header {
27
+ background: linear-gradient(135deg, #1a472a 0%, #2ecc71 100%);
28
+ padding: 1.5rem 2rem;
29
+ border-radius: 12px;
30
+ margin-bottom: 1.5rem;
31
+ box-shadow: 0 4px 20px rgba(46, 204, 113, 0.3);
32
+ }
33
+ .main-header h1 { color: white; margin: 0; font-size: 2rem; }
34
+ .main-header p { color: rgba(255,255,255,0.9); margin: 0.5rem 0 0 0; }
35
+ [data-testid="stMetricValue"] { font-size: 2rem; }
36
+ .hero-metric {
37
+ background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
38
+ padding: 1.5rem;
39
+ border-radius: 12px;
40
+ border-left: 4px solid #2ecc71;
41
+ margin-bottom: 1rem;
42
+ }
43
+ .hero-metric h2 { color: #2ecc71; margin: 0; font-size: 2.5rem; }
44
+ .hero-metric p { color: #888; margin: 0.3rem 0 0 0; font-size: 0.9rem; }
45
+ .tier-header {
46
+ color: #888;
47
+ font-size: 0.75rem;
48
+ text-transform: uppercase;
49
+ letter-spacing: 1px;
50
+ margin-bottom: 0.5rem;
51
+ }
52
+ .comparison-card {
53
+ background: #2c3e50;
54
+ padding: 1.5rem;
55
+ border-radius: 12px;
56
+ text-align: center;
57
+ }
58
+ .winner { border: 2px solid #2ecc71; }
59
+ </style>
60
+ """, unsafe_allow_html=True)
61
+
62
+ # Header
63
+ st.markdown("""
64
+ <div class="main-header">
65
+ <h1>💼 LiiS Strategy Dashboard</h1>
66
+ <p>C-Level View — Portfolio Economics & Break-Even Analysis</p>
67
+ </div>
68
+ """, unsafe_allow_html=True)
69
+
70
+ # ============================================================
71
+ # SIDEBAR - INPUT CONTROLS (4 Sliders)
72
+ # ============================================================
73
+ st.sidebar.header("Portfolio Parameters")
74
+ st.sidebar.caption("Adjust to see impact on profitability")
75
+
76
+ portfolio_size = st.sidebar.slider(
77
+ "Product Portfolio",
78
+ min_value=50, max_value=1000, value=300, step=50,
79
+ help="Number of units in your fleet"
80
+ )
81
+
82
+ monthly_lease = st.sidebar.slider(
83
+ "Monthly Lease Price",
84
+ min_value=250, max_value=650, value=250, step=25,
85
+ format="%d NOK",
86
+ help="What customers pay per month"
87
+ )
88
+
89
+ mfg_cost = st.sidebar.slider(
90
+ "Manufacturing Cost",
91
+ min_value=2500, max_value=7500, value=2500, step=250,
92
+ format="%d NOK",
93
+ help="Cost to produce one unit"
94
+ )
95
+
96
+ reuse_cycles = st.sidebar.slider(
97
+ "Reuse Cycles",
98
+ min_value=2, max_value=10, value=3, step=1,
99
+ help="How many families use each product"
100
+ )
101
+
102
+ # ============================================================
103
+ # CALCULATIONS
104
+ # ============================================================
105
+
106
+ # Per-product economics
107
+ lease_term_months = 12 # Each cycle is 12 months
108
+ revenue_per_product = monthly_lease * lease_term_months * reuse_cycles
109
+
110
+ # Costs per product (simplified model)
111
+ logistics_per_cycle = 150 # NOK - pickup + delivery per cycle
112
+ repair_per_cycle = 200 # NOK - average repair/refurb per cycle
113
+ total_cost_per_product = mfg_cost + (logistics_per_cycle + repair_per_cycle) * reuse_cycles
114
+ blended_cost_per_product = total_cost_per_product / reuse_cycles
115
+
116
+ # Profit per product
117
+ profit_per_product = revenue_per_product - total_cost_per_product
118
+ operating_margin = (profit_per_product / revenue_per_product) * 100 if revenue_per_product > 0 else 0
119
+
120
+ # Portfolio level
121
+ revenue_portfolio = portfolio_size * revenue_per_product
122
+ cost_portfolio = portfolio_size * total_cost_per_product
123
+ blended_cost_portfolio = portfolio_size * blended_cost_per_product
124
+ profit_portfolio = portfolio_size * profit_per_product
125
+
126
+ # Break-even calculation (months)
127
+ monthly_revenue_portfolio = portfolio_size * monthly_lease
128
+ monthly_cost_portfolio = portfolio_size * (repair_per_cycle / lease_term_months) # Monthly degradation
129
+ initial_investment = portfolio_size * mfg_cost
130
+
131
+ # Cumulative profit over time to find break-even
132
+ breakeven_month = None
133
+ cumulative = -initial_investment
134
+ for month in range(1, 121):
135
+ # Add logistics cost at cycle transitions
136
+ if month % lease_term_months == 0:
137
+ cumulative -= portfolio_size * logistics_per_cycle
138
+ cumulative += monthly_revenue_portfolio - monthly_cost_portfolio
139
+ if cumulative >= 0 and breakeven_month is None:
140
+ breakeven_month = month
141
+ break
142
+
143
+ if breakeven_month is None:
144
+ breakeven_month = "> 120"
145
+
146
+ # Linear model comparison
147
+ linear_margin = 0.20 # 20% typical retail margin
148
+ linear_profit_per_sale = mfg_cost * linear_margin / (1 - linear_margin) # Selling at cost + margin
149
+ linear_revenue_per_product = mfg_cost / (1 - linear_margin)
150
+ linear_breakeven_units = int(initial_investment / linear_profit_per_sale) if linear_profit_per_sale > 0 else 999
151
+ # Time to sell that many units (assume 1 sale per unit in portfolio per cycle)
152
+ linear_breakeven_months = max(24, int(linear_breakeven_units / portfolio_size * lease_term_months))
153
+
154
+ # ============================================================
155
+ # TIER 1: PRIMARY METRICS
156
+ # ============================================================
157
+ st.markdown('<p class="tier-header">Tier 1 — Primary Metrics</p>', unsafe_allow_html=True)
158
+
159
+ hero_col1, hero_col2 = st.columns(2)
160
+
161
+ with hero_col1:
162
+ st.markdown(f"""
163
+ <div class="hero-metric">
164
+ <p>BREAK EVEN</p>
165
+ <h2>{breakeven_month} MONTHS</h2>
166
+ <p>For entire portfolio of {portfolio_size:,} units</p>
167
+ </div>
168
+ """, unsafe_allow_html=True)
169
+
170
+ with hero_col2:
171
+ profit_color = "#2ecc71" if profit_portfolio > 0 else "#e74c3c"
172
+ st.markdown(f"""
173
+ <div class="hero-metric">
174
+ <p>TOTAL PORTFOLIO PROFIT</p>
175
+ <h2 style="color: {profit_color}">{profit_portfolio:,.0f} NOK</h2>
176
+ <p>Net revenue across {portfolio_size:,} units over {reuse_cycles} cycles</p>
177
+ </div>
178
+ """, unsafe_allow_html=True)
179
+
180
+ st.markdown("---")
181
+
182
+ # ============================================================
183
+ # TIER 2: SUPPORTING METRICS
184
+ # ============================================================
185
+ st.markdown('<p class="tier-header">Tier 2 — Unit Economics</p>', unsafe_allow_html=True)
186
+
187
+ metric_col1, metric_col2, metric_col3 = st.columns(3)
188
+
189
+ with metric_col1:
190
+ st.metric(
191
+ "Revenue per Product",
192
+ f"{revenue_per_product:,.0f} NOK",
193
+ delta=f"{monthly_lease} × {lease_term_months}mo × {reuse_cycles} cycles",
194
+ help="Total revenue generated by one product over its lifetime"
195
+ )
196
+ st.metric(
197
+ "Revenue per Portfolio",
198
+ f"{revenue_portfolio:,.0f} NOK",
199
+ delta=f"{portfolio_size:,} units",
200
+ help="Total portfolio revenue"
201
+ )
202
+
203
+ with metric_col2:
204
+ st.metric(
205
+ "Blended Cost per Product",
206
+ f"{blended_cost_per_product:,.0f} NOK",
207
+ delta=f"Total {total_cost_per_product:,.0f} ÷ {reuse_cycles} cycles",
208
+ help="Average cost per cycle (manufacturing + logistics + repair)"
209
+ )
210
+ st.metric(
211
+ "Blended Cost per Portfolio",
212
+ f"{blended_cost_portfolio:,.0f} NOK",
213
+ delta=f"{portfolio_size:,} units",
214
+ help="Total portfolio blended cost"
215
+ )
216
+
217
+ with metric_col3:
218
+ margin_color = "normal" if operating_margin >= 50 else "off"
219
+ st.metric(
220
+ "Operating Margin",
221
+ f"{operating_margin:.0f}%",
222
+ delta="healthy" if operating_margin >= 50 else "review pricing",
223
+ delta_color=margin_color,
224
+ help="Profit / Revenue - same for all units"
225
+ )
226
+ st.metric(
227
+ "Profit per Product",
228
+ f"{profit_per_product:,.0f} NOK",
229
+ delta="lifetime profit",
230
+ help="Revenue - Total Cost per product"
231
+ )
232
+
233
+ st.markdown("---")
234
+
235
+ # ============================================================
236
+ # CHART 1: Break Even Comparison
237
+ # ============================================================
238
+ st.markdown('<p class="tier-header">Tier 2 — Model Comparison</p>', unsafe_allow_html=True)
239
+ st.subheader("Break Even: Circular vs Linear Model")
240
+
241
+ comp_col1, comp_col2 = st.columns(2)
242
+
243
+ with comp_col1:
244
+ st.markdown(f"""
245
+ <div class="comparison-card">
246
+ <p style="color: #888; margin: 0;">LINEAR MODEL</p>
247
+ <h2 style="color: #e74c3c; margin: 0.5rem 0;">{linear_breakeven_months} MONTHS</h2>
248
+ <p style="color: #888; margin: 0;">{linear_margin*100:.0f}% margin</p>
249
+ <p style="color: #666; font-size: 0.8rem; margin-top: 0.5rem;">Sell once, done</p>
250
+ </div>
251
+ """, unsafe_allow_html=True)
252
+
253
+ with comp_col2:
254
+ st.markdown(f"""
255
+ <div class="comparison-card winner">
256
+ <p style="color: #888; margin: 0;">CIRCULAR MODEL</p>
257
+ <h2 style="color: #2ecc71; margin: 0.5rem 0;">{breakeven_month} MONTHS</h2>
258
+ <p style="color: #2ecc71; margin: 0;">{operating_margin:.0f}% margin ← BETTER</p>
259
+ <p style="color: #666; font-size: 0.8rem; margin-top: 0.5rem;">Reuse {reuse_cycles}× per product</p>
260
+ </div>
261
+ """, unsafe_allow_html=True)
262
+
263
+ st.markdown("---")
264
+
265
+ # ============================================================
266
+ # CHART 2: Blended Cost vs Revenue Across Cycles
267
+ # ============================================================
268
+ st.subheader("Cost & Revenue Trajectory by Reuse Cycles")
269
+
270
+ cycles_range = list(range(2, 11))
271
+ chart_data = []
272
+
273
+ for c in cycles_range:
274
+ rev = monthly_lease * lease_term_months * c
275
+ total_cost = mfg_cost + (logistics_per_cycle + repair_per_cycle) * c
276
+ blended = total_cost / c
277
+ profit = rev - total_cost
278
+ margin = (profit / rev) * 100 if rev > 0 else 0
279
+
280
+ chart_data.append({
281
+ "Cycles": c,
282
+ "Revenue": rev,
283
+ "Total Cost": total_cost,
284
+ "Blended Cost": blended,
285
+ "Profit": profit,
286
+ "Margin": margin,
287
+ })
288
+
289
+ chart_df = pd.DataFrame(chart_data)
290
+
291
+ fig_cycles = go.Figure()
292
+
293
+ # Revenue line (constant per cycle × cycles)
294
+ fig_cycles.add_trace(go.Scatter(
295
+ x=chart_df["Cycles"],
296
+ y=chart_df["Revenue"],
297
+ mode="lines+markers",
298
+ name="Revenue per Product",
299
+ line=dict(color="#2ecc71", width=3),
300
+ marker=dict(size=8),
301
+ ))
302
+
303
+ # Blended cost line (decreasing)
304
+ fig_cycles.add_trace(go.Scatter(
305
+ x=chart_df["Cycles"],
306
+ y=chart_df["Blended Cost"],
307
+ mode="lines+markers",
308
+ name="Blended Cost per Product",
309
+ line=dict(color="#e74c3c", width=3),
310
+ marker=dict(size=8),
311
+ ))
312
+
313
+ # Total cost line
314
+ fig_cycles.add_trace(go.Scatter(
315
+ x=chart_df["Cycles"],
316
+ y=chart_df["Total Cost"],
317
+ mode="lines+markers",
318
+ name="Total Cost per Product",
319
+ line=dict(color="#e74c3c", width=2, dash="dot"),
320
+ marker=dict(size=6),
321
+ opacity=0.6,
322
+ ))
323
+
324
+ # Mark current cycles
325
+ fig_cycles.add_vline(
326
+ x=reuse_cycles, line_dash="dash", line_color="#f1c40f",
327
+ annotation_text=f"Current: {reuse_cycles} cycles",
328
+ annotation_position="top"
329
+ )
330
+
331
+ fig_cycles.update_layout(
332
+ xaxis_title="Reuse Cycles",
333
+ yaxis_title="NOK per Product",
334
+ legend=dict(orientation="h", yanchor="bottom", y=1.02),
335
+ hovermode="x unified",
336
+ )
337
+
338
+ st.plotly_chart(fig_cycles, use_container_width=True)
339
+
340
+ st.info(f"**Key insight:** More cycles = lower blended cost = higher profit. At {reuse_cycles} cycles, blended cost is {blended_cost_per_product:,.0f} NOK vs {mfg_cost:,} NOK manufacturing cost.")
341
+
342
+ st.markdown("---")
343
+
344
+ # ============================================================
345
+ # TIER 3: AT SCALE
346
+ # ============================================================
347
+ st.markdown('<p class="tier-header">Tier 3 — Scale Understanding</p>', unsafe_allow_html=True)
348
+ st.subheader("At Scale: Customer Base Projection")
349
+
350
+ scale_col1, scale_col2 = st.columns([1, 2])
351
+
352
+ with scale_col1:
353
+ customer_base = st.slider(
354
+ "Customer Base",
355
+ min_value=500, max_value=10000, value=1000, step=100,
356
+ help="Target number of active customers"
357
+ )
358
+
359
+ utilization_rate = 0.80 # 80% of fleet in use at any time
360
+ fleet_needed = int(customer_base / utilization_rate)
361
+
362
+ annual_revenue_scale = customer_base * monthly_lease * lease_term_months
363
+ total_cost_scale = fleet_needed * blended_cost_per_product
364
+ annual_profit_scale = annual_revenue_scale - total_cost_scale
365
+
366
+ st.metric("Fleet Size Needed", f"{fleet_needed:,} units", delta=f"{utilization_rate*100:.0f}% utilization")
367
+ st.metric("Annual Revenue", f"{annual_revenue_scale:,.0f} NOK", delta=f"{customer_base:,} customers")
368
+ st.metric("Total Blended Cost", f"{total_cost_scale:,.0f} NOK", delta=f"{fleet_needed:,} units")
369
+
370
+ profit_delta = "profit" if annual_profit_scale > 0 else "loss"
371
+ st.metric("Annual Profit", f"{annual_profit_scale:,.0f} NOK", delta=profit_delta)
372
+
373
+ with scale_col2:
374
+ # Chart 3: 5-Year Cumulative Profit
375
+ st.markdown("**5-Year Cumulative Profit Trajectory**")
376
+
377
+ # Calculate with 10% utilization decline per cycle
378
+ months = list(range(0, 61))
379
+ cumulative_profit = []
380
+ cumulative = -fleet_needed * mfg_cost # Initial investment
381
+ current_utilization = utilization_rate
382
+
383
+ for month in months:
384
+ if month == 0:
385
+ cumulative_profit.append(cumulative)
386
+ continue
387
+
388
+ # Decline utilization at each cycle end
389
+ if month > 0 and month % lease_term_months == 0:
390
+ current_utilization = max(0.5, current_utilization - 0.10)
391
+ cumulative -= fleet_needed * logistics_per_cycle # Logistics at cycle transition
392
+
393
+ active_customers = int(fleet_needed * current_utilization)
394
+ monthly_rev = active_customers * monthly_lease
395
+ monthly_cost = active_customers * (repair_per_cycle / lease_term_months)
396
+ cumulative += monthly_rev - monthly_cost
397
+ cumulative_profit.append(cumulative)
398
+
399
+ fig_trajectory = go.Figure()
400
+
401
+ fig_trajectory.add_trace(go.Scatter(
402
+ x=months,
403
+ y=cumulative_profit,
404
+ mode="lines",
405
+ name="Cumulative Profit",
406
+ line=dict(color="#2ecc71", width=3),
407
+ fill="tozeroy",
408
+ fillcolor="rgba(46, 204, 113, 0.2)",
409
+ ))
410
+
411
+ # Add zero line
412
+ fig_trajectory.add_hline(y=0, line_color="rgba(255,255,255,0.5)", line_width=1)
413
+
414
+ # Mark cycle transitions
415
+ for cycle_month in range(lease_term_months, 61, lease_term_months):
416
+ fig_trajectory.add_vline(
417
+ x=cycle_month, line_dash="dot", line_color="rgba(255,255,255,0.3)",
418
+ )
419
+
420
+ # Find and mark breakeven
421
+ for i, cp in enumerate(cumulative_profit):
422
+ if cp >= 0 and i > 0:
423
+ fig_trajectory.add_trace(go.Scatter(
424
+ x=[i], y=[cp],
425
+ mode="markers+text",
426
+ name="Breakeven",
427
+ marker=dict(size=12, color="#f1c40f", symbol="star"),
428
+ text=["Breakeven"],
429
+ textposition="top center",
430
+ ))
431
+ break
432
+
433
+ fig_trajectory.update_layout(
434
+ xaxis_title="Months",
435
+ yaxis_title="Cumulative Profit (NOK)",
436
+ showlegend=False,
437
+ yaxis=dict(tickformat=","),
438
+ )
439
+
440
+ st.plotly_chart(fig_trajectory, use_container_width=True)
441
+
442
+ final_profit = cumulative_profit[-1]
443
+ st.caption(f"5-year projection with {fleet_needed:,} unit fleet. Final cumulative profit: **{final_profit:,.0f} NOK**")
444
+
445
+ # ============================================================
446
+ # FOOTER
447
+ # ============================================================
448
+ st.markdown("---")
449
+ st.caption(f"Portfolio: {portfolio_size:,} units | Lease: {monthly_lease} NOK/mo | Mfg: {mfg_cost:,} NOK | Cycles: {reuse_cycles}")