Spaces:
Sleeping
Sleeping
Upload 8 files
Browse files- Dockerfile +29 -0
- calculator.py +152 -0
- config.py +30 -0
- index.html +209 -0
- main.py +64 -0
- requirements.txt +4 -0
- script.js +87 -0
- 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 |
+
© 2026 Water Intelligence Systems • 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 |
+
}
|