shivamsshhiivvaamm commited on
Commit
0a87e25
·
verified ·
1 Parent(s): 056a0ce

Upload 8 files

Browse files
Files changed (8) hide show
  1. Dockerfile +29 -0
  2. calculator.py +152 -0
  3. config.py +30 -0
  4. index.html +209 -0
  5. main.py +64 -0
  6. requirements.txt +4 -0
  7. script.js +87 -0
  8. style.css +280 -0
Dockerfile ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Python runtime as a parent image
2
+ FROM python:3.11-slim
3
+
4
+ # Set up a new user named "user" with user ID 1000 for security (common for HF Spaces)
5
+ RUN useradd -m -u 1000 user
6
+
7
+ # Set the working directory in the container
8
+ WORKDIR /app
9
+
10
+ # Copy the requirements file and install dependencies first (leverages cache)
11
+ COPY --chown=user requirements.txt .
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ # Copy the rest of the application code
15
+ COPY --chown=user . .
16
+
17
+ # Set environment variables
18
+ ENV PYTHONPATH=/app
19
+ ENV PORT=7860
20
+ ENV PYTHONUNBUFFERED=1
21
+
22
+ # Switch to the "user" user
23
+ USER user
24
+
25
+ # Hugging Face Spaces expects port 7860
26
+ EXPOSE 7860
27
+
28
+ # Run the application
29
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
calculator.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from config import FLOW_RATES, USAGE_PATTERNS, USER_MULTIPLIERS, DRINKING_WATER
2
+
3
+ def calculate_usage(requirements):
4
+ """
5
+ Calculates daily water usage and survival days.
6
+
7
+ IMPORTANT - How effective multipliers work (matching Excel model):
8
+ - eff_shower/sink = SUM of (count × mult) across all user types
9
+ → This already encodes total group usage, so NO extra × num_adults needed
10
+ - eff_toilet = SUM of all adult counts (toilet mult = 1 for everyone)
11
+ → Used for display only; toilet formula uses base per-person × 1 event
12
+ - Toilet daily = flow × frequency × 1 (base, not scaled by eff_toilet)
13
+ - Drinking adults = 0.7 × num_adults from CORE PARAMS (not user type total)
14
+ """
15
+
16
+ # ── User type counts (from User Type Distribution) ──────────────────────
17
+ num_expert = requirements.get("num_expert", 0)
18
+ num_typical = requirements.get("num_typical", 0)
19
+ num_glamper = requirements.get("num_glamper", 0)
20
+
21
+ # ── Core params ──────────────────────────────────────────────────────────
22
+ num_adults = requirements.get("num_adults", num_expert + num_typical + num_glamper)
23
+ num_children = requirements.get("num_children", 0)
24
+ climate_mult = requirements.get("climate_mult", 1.0)
25
+
26
+ # ── Effective Multipliers (SUM — already encodes all adults) ─────────────
27
+ # Excel: Eff Shower = 2×0.6 + 0×1.0 + 1×1.5 = 2.7 (NOT divided by adults)
28
+ eff_shower_mult = (
29
+ num_expert * USER_MULTIPLIERS["expert"]["shower"] +
30
+ num_typical * USER_MULTIPLIERS["typical"]["shower"] +
31
+ num_glamper * USER_MULTIPLIERS["glamper"]["shower"]
32
+ )
33
+
34
+ eff_sink_mult = (
35
+ num_expert * USER_MULTIPLIERS["expert"]["sink"] +
36
+ num_typical * USER_MULTIPLIERS["typical"]["sink"] +
37
+ num_glamper * USER_MULTIPLIERS["glamper"]["sink"]
38
+ )
39
+
40
+ # Toilet mult = 1 for all types → eff_toilet = total adults (display only)
41
+ eff_toilet_mult = num_expert + num_typical + num_glamper
42
+
43
+ # ── Daily Fresh Water Per Activity ───────────────────────────────────────
44
+
45
+ # Shower: flow × duration × frequency × eff_shower_mult × climate
46
+ # (eff_shower_mult already accounts for all adults — no × num_adults)
47
+ shower_daily = (
48
+ FLOW_RATES["shower"] *
49
+ USAGE_PATTERNS["shower"]["duration"] *
50
+ USAGE_PATTERNS["shower"]["frequency"] *
51
+ eff_shower_mult *
52
+ climate_mult
53
+ )
54
+
55
+ # Kitchen Sink: same pattern
56
+ kitchen_sink_daily = (
57
+ FLOW_RATES["kitchen_sink"] *
58
+ USAGE_PATTERNS["kitchen_sink"]["duration"] *
59
+ USAGE_PATTERNS["kitchen_sink"]["frequency"] *
60
+ eff_sink_mult *
61
+ climate_mult
62
+ )
63
+
64
+ # Bathroom Sink: same pattern
65
+ bathroom_sink_daily = (
66
+ FLOW_RATES["bathroom_sink"] *
67
+ USAGE_PATTERNS["bathroom_sink"]["duration"] *
68
+ USAGE_PATTERNS["bathroom_sink"]["frequency"] *
69
+ eff_sink_mult *
70
+ climate_mult
71
+ )
72
+
73
+ # Toilet: base per-person usage only (1.8 gal × 6 flushes × 1 person base)
74
+ # Excel shows 10.8 regardless of group size in activity table
75
+ toilet_daily = (
76
+ FLOW_RATES["toilet"] *
77
+ USAGE_PATTERNS["toilet"]["frequency"] *
78
+ 1 # base single-person; eff_toilet is display only
79
+ )
80
+
81
+ # Drinking: uses core num_adults (e.g., 2) not user-type total
82
+ drinking_adults_daily = DRINKING_WATER["adult"] * num_adults
83
+ drinking_children_daily = DRINKING_WATER["child"] * num_children
84
+ total_drinking_daily = drinking_adults_daily + drinking_children_daily
85
+
86
+ # ── Totals ───────────────────────────────────────────────────────────────
87
+ total_fresh_used_per_day = (
88
+ shower_daily + kitchen_sink_daily + bathroom_sink_daily +
89
+ toilet_daily + total_drinking_daily
90
+ )
91
+ total_grey_added_per_day = shower_daily + kitchen_sink_daily + bathroom_sink_daily
92
+ total_black_added_per_day = toilet_daily
93
+
94
+ # ── Tank Levels ──────────────────────────────────────────────────────────
95
+ fresh_cap = requirements.get("fresh_cap", 0)
96
+ grey_cap = requirements.get("grey_cap", 0)
97
+ black_cap = requirements.get("black_cap", 0)
98
+ fresh_level = requirements.get("fresh_level", 0)
99
+ grey_level = requirements.get("grey_level", 0)
100
+ black_level = requirements.get("black_level", 0)
101
+
102
+ # ── Survival Days ────────────────────────────────────────────────────────
103
+ # Fresh: runs DOWN → days until empty
104
+ # Grey: fills UP → days until full
105
+ # Black: fills UP → days until full
106
+ days_fresh = (fresh_level / total_fresh_used_per_day
107
+ if total_fresh_used_per_day > 0 and fresh_level > 0
108
+ else 0.0)
109
+
110
+ days_grey = ((grey_cap - grey_level) / total_grey_added_per_day
111
+ if total_grey_added_per_day > 0
112
+ else float('inf'))
113
+
114
+ days_black = ((black_cap - black_level) / total_black_added_per_day
115
+ if total_black_added_per_day > 0
116
+ else float('inf'))
117
+
118
+ overall_survival = min(days_fresh, days_grey, days_black)
119
+
120
+ # Limiting factor
121
+ tank_map = {days_fresh: "Fresh", days_grey: "Grey", days_black: "Black"}
122
+ limiting_tank = tank_map[min(days_fresh, days_grey, days_black)]
123
+
124
+ return {
125
+ "daily_usage": {
126
+ "fresh": round(total_fresh_used_per_day, 2),
127
+ "grey": round(total_grey_added_per_day, 2),
128
+ "black": round(total_black_added_per_day, 2),
129
+ "drinking": round(total_drinking_daily, 2),
130
+ "shower": round(shower_daily, 2),
131
+ "kitchen_sink": round(kitchen_sink_daily, 2),
132
+ "bathroom_sink": round(bathroom_sink_daily, 2),
133
+ "toilet": round(toilet_daily, 2),
134
+ },
135
+ "survival_days": {
136
+ "fresh": round(days_fresh, 4),
137
+ "grey": round(days_grey, 4),
138
+ "black": round(days_black, 4),
139
+ "overall": round(overall_survival, 4),
140
+ "limiting_tank": limiting_tank,
141
+ },
142
+ "effective_multipliers": {
143
+ "shower": round(eff_shower_mult, 3),
144
+ "sink": round(eff_sink_mult, 3),
145
+ "toilet": eff_toilet_mult,
146
+ },
147
+ "tanks": {
148
+ "fresh": {"cap": fresh_cap, "level": fresh_level},
149
+ "grey": {"cap": grey_cap, "level": grey_level},
150
+ "black": {"cap": black_cap, "level": black_level}
151
+ }
152
+ }
config.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Water Management System Configuration
2
+
3
+ # Flow rates (gallons per minute or flushes)
4
+ FLOW_RATES = {
5
+ "shower": 1.9,
6
+ "kitchen_sink": 1.5,
7
+ "toilet": 1.8, # gallons per flush
8
+ "bathroom_sink": 1.0
9
+ }
10
+
11
+ # Durations and frequencies
12
+ USAGE_PATTERNS = {
13
+ "shower": {"duration": 5, "frequency": 1},
14
+ "kitchen_sink": {"duration": 3, "frequency": 3},
15
+ "toilet": {"frequency": 6},
16
+ "bathroom_sink": {"duration": 0.5, "frequency": 4}
17
+ }
18
+
19
+ # Multipliers for user types
20
+ USER_MULTIPLIERS = {
21
+ "expert": {"shower": 0.6, "sink": 0.7},
22
+ "typical": {"shower": 1.0, "sink": 1.0},
23
+ "glamper": {"shower": 1.5, "sink": 1.4}
24
+ }
25
+
26
+ # Drinking water (gallons per day)
27
+ DRINKING_WATER = {
28
+ "adult": 0.7,
29
+ "child": 0.35
30
+ }
index.html ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Off-grid calculator</title>
8
+ <link rel="stylesheet" href="style.css">
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link
12
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&family=Outfit:wght@400;700&display=swap"
13
+ rel="stylesheet">
14
+ </head>
15
+
16
+ <body>
17
+ <div class="container">
18
+ <header>
19
+ <h1>Off-grid calculator</h1>
20
+ <div class="nav-buttons">
21
+ <button class="nav-btn">Range Calculator</button>
22
+ <button class="nav-btn active">Off-Grid Calculator</button>
23
+ </div>
24
+ </header>
25
+
26
+ <p class="subtitle">Estimates are based on a single charge with a full fuel tank. Actual results will vary by
27
+ usage and conditions.</p>
28
+
29
+ <main class="dashboard-grid">
30
+ <!-- Configuration Column -->
31
+ <section class="config-column">
32
+ <div class="card">
33
+ <h2>Number of Travellers</h2>
34
+ <div class="input-grid">
35
+ <div class="input-group">
36
+ <label>Expert Adults</label>
37
+ <div class="input-wrapper">
38
+ <input type="number" id="num_expert" min="0" value="0">
39
+ </div>
40
+ </div>
41
+ <div class="input-group">
42
+ <label>Typical Adults</label>
43
+ <div class="input-wrapper">
44
+ <input type="number" id="num_typical" min="0" value="1">
45
+ </div>
46
+ </div>
47
+ <div class="input-group">
48
+ <label>Glamper Adults</label>
49
+ <div class="input-wrapper">
50
+ <input type="number" id="num_glamper" min="0" value="0">
51
+ </div>
52
+ </div>
53
+ <div class="input-group">
54
+ <label>Children</label>
55
+ <div class="input-wrapper">
56
+ <input type="number" id="num_children" min="0" value="0">
57
+ </div>
58
+ </div>
59
+ </div>
60
+ </div>
61
+
62
+ <div class="card">
63
+ <h2>Tank Capacities (Gallons)</h2>
64
+ <div class="input-grid">
65
+ <div class="input-group">
66
+ <label>Fresh Tank</label>
67
+ <div class="input-wrapper">
68
+ <input type="number" id="fresh_cap" min="0" value="50">
69
+ </div>
70
+ </div>
71
+ <div class="input-group">
72
+ <label>Grey Tank</label>
73
+ <div class="input-wrapper">
74
+ <input type="number" id="grey_cap" min="0" value="40">
75
+ </div>
76
+ </div>
77
+ <div class="input-group">
78
+ <label>Black Tank</label>
79
+ <div class="input-wrapper">
80
+ <input type="number" id="black_cap" min="0" value="30">
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </div>
85
+
86
+ <div class="card">
87
+ <h2>Current Levels (Gallons)</h2>
88
+ <div class="input-grid">
89
+ <div class="input-group">
90
+ <label>Current Fresh</label>
91
+ <div class="input-wrapper">
92
+ <input type="number" id="fresh_level" min="0" value="50">
93
+ </div>
94
+ </div>
95
+ <div class="input-group">
96
+ <label>Current Grey</label>
97
+ <div class="input-wrapper">
98
+ <input type="number" id="grey_level" min="0" value="0">
99
+ </div>
100
+ </div>
101
+ <div class="input-group">
102
+ <label>Current Black</label>
103
+ <div class="input-wrapper">
104
+ <input type="number" id="black_level" min="0" value="0">
105
+ </div>
106
+ </div>
107
+ </div>
108
+ </div>
109
+
110
+ <div class="card">
111
+ <h2>Environment</h2>
112
+ <div class="input-grid">
113
+ <div class="input-group">
114
+ <label>Climate Multiplier</label>
115
+ <div class="input-wrapper">
116
+ <input type="number" id="climate_mult" min="0.1" step="0.1" value="1.0">
117
+ </div>
118
+ </div>
119
+ </div>
120
+ </div>
121
+
122
+ <button id="calculate-btn">Calculate Survival Status</button>
123
+ </section>
124
+
125
+ <!-- Visuals/Results Column -->
126
+ <section class="visuals-column">
127
+ <div class="card">
128
+ <div class="usage-bars">
129
+ <div class="bar-item">
130
+ <div class="bar-info">
131
+ <span>FRESH WATER USAGE</span>
132
+ <span id="val-fresh">0.00 gal</span>
133
+ </div>
134
+ <div class="bar-track">
135
+ <div id="fill-fresh" class="bar-fill"></div>
136
+ </div>
137
+ </div>
138
+ <div class="bar-item">
139
+ <div class="bar-info">
140
+ <span>GREY WATER ADDED</span>
141
+ <span id="val-grey">0.00 gal</span>
142
+ </div>
143
+ <div class="bar-track">
144
+ <div id="fill-grey" class="bar-fill"></div>
145
+ </div>
146
+ </div>
147
+ <div class="bar-item">
148
+ <div class="bar-info">
149
+ <span>BLACK WATER ADDED</span>
150
+ <span id="val-black">0.00 gal</span>
151
+ </div>
152
+ <div class="bar-track">
153
+ <div id="fill-black" class="bar-fill"></div>
154
+ </div>
155
+ </div>
156
+ </div>
157
+
158
+ <div class="projection-section">
159
+ <h2>Tank Projection</h2>
160
+ <table class="projection-table">
161
+ <thead>
162
+ <tr>
163
+ <th>Resource</th>
164
+ <th>Capacity</th>
165
+ <th>Current Level</th>
166
+ <th>Daily Change</th>
167
+ <th>Days Off-Grid</th>
168
+ </tr>
169
+ </thead>
170
+ <tbody id="projection-body">
171
+ <!-- Populated via JS -->
172
+ </tbody>
173
+ </table>
174
+ </div>
175
+ </div>
176
+
177
+ <div class="summary-visual">
178
+ <!-- Standard high-quality placeholder if generation failed -->
179
+ <img src="https://images.unsplash.com/photo-1523987355523-c7b5b0dd90a7?auto=format&fit=crop&q=80&w=1200"
180
+ alt="Expedition Vehicle">
181
+ <div class="summary-overlay">
182
+ <div class="overall-stat">
183
+ <div class="stat-box">
184
+ <h4>Daily Power Usage</h4>
185
+ <p>0.0 <span style="font-size: 0.8rem; color: var(--text-dim);">kWh</span></p>
186
+ </div>
187
+ <div class="stat-box">
188
+ <h4>Daily Water Usage</h4>
189
+ <p id="daily-water-total">0.0 <span
190
+ style="font-size: 0.8rem; color: var(--text-dim);">gallons</span></p>
191
+ </div>
192
+ </div>
193
+ <div class="survival-big">
194
+ <h3>Off-Grid Duration</h3>
195
+ <span id="overall-days-big">--</span>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ </section>
200
+ </main>
201
+
202
+ <footer style="margin-top: 4rem; text-align: center; color: var(--text-dim); font-size: 0.8rem;">
203
+ &copy; 2026 Water Intelligence Systems &bull; Smart Resource Management
204
+ </footer>
205
+ </div>
206
+ <script src="script.js"></script>
207
+ </body>
208
+
209
+ </html>
main.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from fastapi.staticfiles import StaticFiles
3
+ from fastapi.responses import FileResponse
4
+ from pydantic import BaseModel
5
+ import os
6
+ import logging
7
+ from calculator import calculate_usage
8
+
9
+ # Configure logging
10
+ logging.basicConfig(level=logging.INFO)
11
+ logger = logging.getLogger(__name__)
12
+
13
+ app = FastAPI(title="Water Intelligence Management System")
14
+
15
+ # Pydantic model for request validation
16
+ class CalculationRequirements(BaseModel):
17
+ num_expert: int = 0
18
+ num_typical: int = 0
19
+ num_glamper: int = 0
20
+ num_children: int = 0
21
+ fresh_cap: float = 0
22
+ grey_cap: float = 0
23
+ black_cap: float = 0
24
+ fresh_level: float = 0
25
+ grey_level: float = 0
26
+ black_level: float = 0
27
+ climate_mult: float = 1.0
28
+
29
+ @app.post("/calculate")
30
+ async def calculate(requirements: CalculationRequirements):
31
+ try:
32
+ results = calculate_usage(requirements.model_dump())
33
+ return results
34
+ except Exception as e:
35
+ logger.error(f"Calculation error: {e}")
36
+ raise HTTPException(status_code=500, detail=str(e))
37
+
38
+ # Setup paths - everything is now in the root
39
+ current_dir = os.path.dirname(os.path.abspath(__file__))
40
+
41
+ logger.info(f"Current directory: {current_dir}")
42
+ logger.info(f"Contents of directory: {os.listdir(current_dir)}")
43
+
44
+ # Serve specific static files to avoid exposing the whole directory
45
+ @app.get("/script.js")
46
+ async def get_js():
47
+ return FileResponse(os.path.join(current_dir, "script.js"))
48
+
49
+ @app.get("/style.css")
50
+ async def get_css():
51
+ return FileResponse(os.path.join(current_dir, "style.css"))
52
+
53
+ @app.get("/")
54
+ async def read_root():
55
+ index_file = os.path.join(current_dir, "index.html")
56
+ if os.path.exists(index_file):
57
+ return FileResponse(index_file)
58
+ logger.error(f"index.html NOT found at {index_file}")
59
+ return {"message": "index.html not found", "path": index_file}
60
+
61
+ if __name__ == "__main__":
62
+ import uvicorn
63
+ port = int(os.environ.get("PORT", 7860))
64
+ uvicorn.run(app, host="0.0.0.0", port=port, forwarded_allow_ips='*')
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ pydantic
4
+ aiofiles
script.js ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.getElementById('calculate-btn').addEventListener('click', async () => {
2
+ const requirements = {
3
+ num_expert: parseInt(document.getElementById('num_expert').value) || 0,
4
+ num_typical: parseInt(document.getElementById('num_typical').value) || 0,
5
+ num_glamper: parseInt(document.getElementById('num_glamper').value) || 0,
6
+ num_children: parseInt(document.getElementById('num_children').value) || 0,
7
+ fresh_cap: parseFloat(document.getElementById('fresh_cap').value) || 0,
8
+ grey_cap: parseFloat(document.getElementById('grey_cap').value) || 0,
9
+ black_cap: parseFloat(document.getElementById('black_cap').value) || 0,
10
+ fresh_level: parseFloat(document.getElementById('fresh_level').value) || 0,
11
+ grey_level: parseFloat(document.getElementById('grey_level').value) || 0,
12
+ black_level: parseFloat(document.getElementById('black_level').value) || 0,
13
+ climate_mult: parseFloat(document.getElementById('climate_mult').value) || 1.0
14
+ };
15
+
16
+ try {
17
+ const response = await fetch('calculate', {
18
+ method: 'POST',
19
+ headers: {
20
+ 'Content-Type': 'application/json'
21
+ },
22
+ body: JSON.stringify(requirements)
23
+ });
24
+
25
+ if (!response.ok) {
26
+ throw new Error('Calculation failed');
27
+ }
28
+
29
+ const data = await response.json();
30
+ updateUI(data);
31
+ } catch (error) {
32
+ console.error('Error:', error);
33
+ alert('An error occurred while calculating. Please check your inputs.');
34
+ }
35
+ });
36
+
37
+ function updateUI(data) {
38
+ // Overall Stats
39
+ const overallDays = data.survival_days.overall;
40
+ const overallDisplay = document.getElementById('overall-days-big');
41
+ overallDisplay.innerText = overallDays === Infinity ? 'Unlimited' : (overallDays > 999 ? '999+' : overallDays.toFixed(1));
42
+
43
+ document.getElementById('daily-water-total').innerHTML = `${data.daily_usage.fresh.toFixed(1)} <span style="font-size: 0.8rem; color: var(--text-dim);">gallons</span>`;
44
+
45
+ // Usage Bars (Animate based on % of capacity used daily, capped at 100%)
46
+ const freshUsagePct = Math.min((data.daily_usage.fresh / (data.tanks.fresh.cap || 1)) * 100, 100);
47
+ const greyAddedPct = Math.min((data.daily_usage.grey / (data.tanks.grey.cap || 1)) * 100, 100);
48
+ const blackAddedPct = Math.min((data.daily_usage.black / (data.tanks.black.cap || 1)) * 100, 100);
49
+
50
+ animateBar('fill-fresh', freshUsagePct);
51
+ animateBar('fill-grey', greyAddedPct);
52
+ animateBar('fill-black', blackAddedPct);
53
+
54
+ document.getElementById('val-fresh').innerText = `${data.daily_usage.fresh.toFixed(2)} gal`;
55
+ document.getElementById('val-grey').innerText = `${data.daily_usage.grey.toFixed(2)} gal`;
56
+ document.getElementById('val-black').innerText = `${data.daily_usage.black.toFixed(2)} gal`;
57
+
58
+ // Populate Table
59
+ const tableBody = document.getElementById('projection-body');
60
+ tableBody.innerHTML = '';
61
+
62
+ const sortedResources = [
63
+ { name: 'Fresh', cap: data.tanks.fresh.cap, level: data.tanks.fresh.level, change: -data.daily_usage.fresh, days: data.survival_days.fresh },
64
+ { name: 'Grey', cap: data.tanks.grey.cap, level: data.tanks.grey.level, change: data.daily_usage.grey, days: data.survival_days.grey },
65
+ { name: 'Black', cap: data.tanks.black.cap, level: data.tanks.black.level, change: data.daily_usage.black, days: data.survival_days.black }
66
+ ];
67
+
68
+ sortedResources.forEach(res => {
69
+ const row = document.createElement('tr');
70
+ row.innerHTML = `
71
+ <td>${res.name}</td>
72
+ <td>${res.cap}</td>
73
+ <td>${res.level.toFixed(1)}</td>
74
+ <td style="color: ${res.change > 0 ? '#ff4444' : '#00ff00'}">${res.change > 0 ? '+' : ''}${res.change.toFixed(2)}</td>
75
+ <td>${res.days === Infinity ? '∞' : res.days.toFixed(1)}</td>
76
+ `;
77
+ tableBody.appendChild(row);
78
+ });
79
+ }
80
+
81
+ function animateBar(id, pct) {
82
+ const bar = document.getElementById(id);
83
+ bar.style.width = '0%';
84
+ setTimeout(() => {
85
+ bar.style.width = `${pct}%`;
86
+ }, 50);
87
+ }
style.css ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --primary-color: #00ff00;
3
+ --bg-dark: #0a0a0a;
4
+ --card-bg: #1a1a1a;
5
+ --text-main: #ffffff;
6
+ --text-dim: #888888;
7
+ --border-color: #333333;
8
+ --accent-glow: rgba(0, 255, 0, 0.2);
9
+ }
10
+
11
+ * {
12
+ box-sizing: border-box;
13
+ margin: 0;
14
+ padding: 0;
15
+ }
16
+
17
+ body {
18
+ font-family: 'Inter', sans-serif;
19
+ background-color: var(--bg-dark);
20
+ color: var(--text-main);
21
+ line-height: 1.6;
22
+ min-height: 100vh;
23
+ }
24
+
25
+ .container {
26
+ max-width: 1200px;
27
+ margin: 0 auto;
28
+ padding: 2rem;
29
+ }
30
+
31
+ header {
32
+ margin-bottom: 2rem;
33
+ display: flex;
34
+ justify-content: space-between;
35
+ align-items: flex-end;
36
+ }
37
+
38
+ header h1 {
39
+ font-family: 'Outfit', sans-serif;
40
+ font-size: 3.5rem;
41
+ color: var(--primary-color);
42
+ letter-spacing: -2px;
43
+ }
44
+
45
+ .nav-buttons {
46
+ display: flex;
47
+ gap: 1rem;
48
+ margin-bottom: 1rem;
49
+ }
50
+
51
+ .nav-btn {
52
+ background: transparent;
53
+ border: 1px solid var(--border-color);
54
+ color: var(--text-dim);
55
+ padding: 0.5rem 1rem;
56
+ font-size: 0.8rem;
57
+ cursor: pointer;
58
+ transition: all 0.3s;
59
+ }
60
+
61
+ .nav-btn.active {
62
+ background: var(--card-bg);
63
+ color: var(--text-main);
64
+ border-color: var(--text-main);
65
+ }
66
+
67
+ .subtitle {
68
+ color: var(--text-dim);
69
+ font-size: 0.9rem;
70
+ margin-bottom: 2rem;
71
+ padding-bottom: 1rem;
72
+ border-bottom: 1px solid var(--border-color);
73
+ }
74
+
75
+ .dashboard-grid {
76
+ display: grid;
77
+ grid-template-columns: 1fr 1fr;
78
+ gap: 2rem;
79
+ }
80
+
81
+ .config-column {
82
+ display: flex;
83
+ flex-direction: column;
84
+ gap: 1rem;
85
+ }
86
+
87
+ .card {
88
+ background: var(--card-bg);
89
+ border: 1px solid var(--border-color);
90
+ padding: 1.5rem;
91
+ border-radius: 4px;
92
+ }
93
+
94
+ .card h2 {
95
+ font-size: 0.8rem;
96
+ text-transform: uppercase;
97
+ color: var(--text-dim);
98
+ margin-bottom: 1rem;
99
+ letter-spacing: 1px;
100
+ }
101
+
102
+ .input-grid {
103
+ display: grid;
104
+ grid-template-columns: repeat(2, 1fr);
105
+ gap: 1rem;
106
+ }
107
+
108
+ .input-group {
109
+ display: flex;
110
+ flex-direction: column;
111
+ gap: 0.5rem;
112
+ }
113
+
114
+ .input-group label {
115
+ font-size: 0.75rem;
116
+ color: var(--text-dim);
117
+ text-transform: uppercase;
118
+ }
119
+
120
+ .input-wrapper {
121
+ display: flex;
122
+ align-items: center;
123
+ background: #111;
124
+ border: 1px solid var(--border-color);
125
+ padding: 0.5rem;
126
+ border-radius: 4px;
127
+ }
128
+
129
+ .input-wrapper input {
130
+ background: transparent;
131
+ border: none;
132
+ color: white;
133
+ font-size: 1.2rem;
134
+ width: 100%;
135
+ outline: none;
136
+ }
137
+
138
+ #calculate-btn {
139
+ margin-top: 1rem;
140
+ background: var(--primary-color);
141
+ color: black;
142
+ border: none;
143
+ padding: 1rem;
144
+ font-weight: bold;
145
+ text-transform: uppercase;
146
+ cursor: pointer;
147
+ transition: filter 0.3s;
148
+ }
149
+
150
+ #calculate-btn:hover {
151
+ filter: brightness(1.2);
152
+ }
153
+
154
+ /* Results Column */
155
+ .visuals-column {
156
+ display: flex;
157
+ flex-direction: column;
158
+ gap: 1.5rem;
159
+ }
160
+
161
+ .usage-bars {
162
+ display: flex;
163
+ flex-direction: column;
164
+ gap: 1rem;
165
+ }
166
+
167
+ .bar-item {
168
+ display: flex;
169
+ flex-direction: column;
170
+ gap: 0.25rem;
171
+ }
172
+
173
+ .bar-info {
174
+ display: flex;
175
+ justify-content: space-between;
176
+ font-size: 0.8rem;
177
+ }
178
+
179
+ .bar-track {
180
+ height: 4px;
181
+ background: #222;
182
+ width: 100%;
183
+ border-radius: 2px;
184
+ }
185
+
186
+ .bar-fill {
187
+ height: 100%;
188
+ background: var(--primary-color);
189
+ width: 0%;
190
+ transition: width 1s ease-out;
191
+ }
192
+
193
+ .projection-section {
194
+ margin-top: 1rem;
195
+ }
196
+
197
+ .projection-table {
198
+ width: 100%;
199
+ border-collapse: collapse;
200
+ font-size: 0.85rem;
201
+ }
202
+
203
+ .projection-table th {
204
+ text-align: left;
205
+ color: var(--text-dim);
206
+ padding: 0.5rem 0;
207
+ font-weight: normal;
208
+ }
209
+
210
+ .projection-table td {
211
+ padding: 0.75rem 0;
212
+ border-bottom: 1px solid var(--border-color);
213
+ }
214
+
215
+ .summary-visual {
216
+ position: relative;
217
+ background: #111;
218
+ border-radius: 4px;
219
+ overflow: hidden;
220
+ aspect-ratio: 16/10;
221
+ }
222
+
223
+ .summary-visual img {
224
+ width: 100%;
225
+ height: 100%;
226
+ object-fit: cover;
227
+ opacity: 0.6;
228
+ }
229
+
230
+ .summary-overlay {
231
+ position: absolute;
232
+ top: 0;
233
+ left: 0;
234
+ width: 100%;
235
+ height: 100%;
236
+ padding: 2rem;
237
+ display: flex;
238
+ flex-direction: column;
239
+ justify-content: space-between;
240
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent);
241
+ }
242
+
243
+ .overall-stat {
244
+ display: flex;
245
+ gap: 2rem;
246
+ }
247
+
248
+ .stat-box h4 {
249
+ font-size: 0.7rem;
250
+ text-transform: uppercase;
251
+ color: var(--text-dim);
252
+ }
253
+
254
+ .stat-box p {
255
+ font-size: 1.5rem;
256
+ font-weight: bold;
257
+ }
258
+
259
+ .survival-big {
260
+ margin-top: auto;
261
+ }
262
+
263
+ .survival-big h3 {
264
+ font-size: 0.7rem;
265
+ text-transform: uppercase;
266
+ color: var(--text-dim);
267
+ }
268
+
269
+ .survival-big span {
270
+ font-size: 4rem;
271
+ display: block;
272
+ line-height: 1;
273
+ font-weight: 800;
274
+ }
275
+
276
+ @media (max-width: 900px) {
277
+ .dashboard-grid {
278
+ grid-template-columns: 1fr;
279
+ }
280
+ }