diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b2d63c9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+utils/__pycache__/__init__.cpython-313.pyc
+utils/__pycache__/geocode.cpython-313.pyc
+utils/__pycache__/weather.cpython-313.pyc
+__pycache__/config.cpython-313.pyc
diff --git a/Dockerfile b/Dockerfile
index bb3e50d..2f70489 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,12 +1,22 @@
+# Use Python 3.12 slim image
FROM python:3.12-slim
+# Set working directory
WORKDIR /app
+# Copy and install dependencies first (cache-friendly)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
+# Copy the rest of the app
COPY app.py .
+COPY config.py .
+COPY utils/ ./utils
+COPY templates/ ./templates
+COPY static/ ./static
+# Expose the Flask port
EXPOSE 5000
-CMD ["gunicorn", "-b", "0.0.0.0:5000", "app:app"]
\ No newline at end of file
+# Run the app with Gunicorn
+CMD ["gunicorn", "-b", "0.0.0.0:5000", "app:app"]
diff --git a/app.py b/app.py
index 3f4246a..11c6af3 100644
--- a/app.py
+++ b/app.py
@@ -1,182 +1,28 @@
-from flask import Flask, render_template_string, jsonify, request
-from datetime import datetime, date
-import requests
-import pytz
-from timezonefinder import TimezoneFinder
+from flask import Flask, render_template, jsonify, request
+from utils.weather import get_today_snowfall
+from utils.geocode import get_city_name
+from config import MAX_SNOW_INCHES
app = Flask(__name__)
-MAX_SNOW_INCHES = 24
-OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast"
-tf = TimezoneFinder()
-
-HTML = """
-
-
-
-
-Are We Buried?
-
-
-
-
-
-
-
-
Are We Buried?
-
-- in
-
--
-
Last updated: --
-
-
-
-
-"""
-
-def get_today_snowfall(lat, lon):
- try:
- # Determine timezone for location
- tz_name = tf.timezone_at(lat=lat, lng=lon) or "UTC"
-
- params = {
- "latitude": lat,
- "longitude": lon,
- "hourly": "snowfall",
- "timezone": tz_name
- }
-
- r = requests.get(OPEN_METEO_URL, params=params, timeout=5)
- r.raise_for_status()
- data = r.json()
-
- hourly = data.get("hourly", {})
- times = hourly.get("time", [])
- snow_values = hourly.get("snowfall", [])
-
- today_str = datetime.now(pytz.timezone(tz_name)).date().isoformat()
-
- snowfall_cm = 0.0
- snowing_now = False
- for t, s in zip(times, snow_values):
- if t.startswith(today_str):
- snowfall_cm += s
- if s > 0:
- snowing_now = True
-
- inches = round(snowfall_cm / 2.54, 1)
- return inches, snowing_now, tz_name
-
- except Exception as e:
- print("Error fetching snowfall:", e)
- return 0.0, False, "UTC"
-
-def get_city_name(lat, lon):
- """Use OpenStreetMap Nominatim to get the city name for coordinates."""
- try:
- r = requests.get(
- "https://nominatim.openstreetmap.org/reverse",
- params={
- "lat": lat,
- "lon": lon,
- "format": "json",
- "zoom": 10, # city-level
- "addressdetails": 1
- },
- headers={"User-Agent": "AreWeBuriedApp/1.0"}
- )
- r.raise_for_status()
- data = r.json()
- address = data.get("address", {})
- city = address.get("city") or address.get("town") or address.get("village") or address.get("county") or f"{lat:.2f},{lon:.2f}"
- return city
- except Exception as e:
- print("Reverse geocoding error:", e)
- return f"{lat:.2f},{lon:.2f}"
-
@app.route("/")
def index():
- return render_template_string(HTML)
+ return render_template("index.html")
@app.route("/api/snowfall")
def snowfall_api():
lat = request.args.get("lat", type=float)
lon = request.args.get("lon", type=float)
-
if lat is None or lon is None:
return jsonify({"error": "Missing lat/lon"}), 400
inches, snowing, tz_name = get_today_snowfall(lat, lon)
- today = datetime.now(pytz.timezone(tz_name)).strftime("%B %d, %Y")
-
city_name = get_city_name(lat, lon)
+ from datetime import datetime
+ import pytz
+ today = datetime.now(pytz.timezone(tz_name)).strftime("%B %d, %Y")
+
return jsonify({
"location": city_name,
"lat": lat,
diff --git a/config.py b/config.py
new file mode 100644
index 0000000..ce22a0c
--- /dev/null
+++ b/config.py
@@ -0,0 +1,2 @@
+MAX_SNOW_INCHES = 24
+OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast"
diff --git a/old.py b/old.py
new file mode 100644
index 0000000..60d7554
--- /dev/null
+++ b/old.py
@@ -0,0 +1,194 @@
+from flask import Flask, render_template_string, jsonify, request
+from datetime import datetime, date
+import requests
+import pytz
+from timezonefinder import TimezoneFinder
+from math import floor
+
+app = Flask(__name__)
+
+MAX_SNOW_INCHES = 24
+OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast"
+tf = TimezoneFinder()
+
+HTML = """
+
+
+
+
+Are We Buried?
+
+
+
+
+
+
+
+
Are We Buried?
+
-- in
+
--
+
Last updated: --
+
+
+
+
+"""
+
+def get_today_snowfall(lat, lon):
+ try:
+ # Determine timezone for location
+ tz_name = tf.timezone_at(lat=lat, lng=lon) or "UTC"
+
+ params = {
+ "latitude": lat,
+ "longitude": lon,
+ "hourly": "snowfall",
+ "timezone": tz_name
+ }
+
+ r = requests.get(OPEN_METEO_URL, params=params, timeout=5)
+ r.raise_for_status()
+ data = r.json()
+
+ hourly = data.get("hourly", {})
+ times = hourly.get("time", [])
+ snow_values = hourly.get("snowfall", [])
+
+ today_str = datetime.now(pytz.timezone(tz_name)).date().isoformat()
+
+ snowfall_cm = 0.0
+ snowing_now = False
+ for t, s in zip(times, snow_values):
+ if t.startswith(today_str):
+ snowfall_cm += s
+ if s > 0:
+ snowing_now = True
+
+ inches = round(snowfall_cm / 2.54, 1)
+ return inches, snowing_now, tz_name
+
+ except Exception as e:
+ print("Error fetching snowfall:", e)
+ return 0.0, False, "UTC"
+
+def get_city_name(lat, lon):
+ """Use OpenStreetMap Nominatim to get the city name for coordinates."""
+ try:
+ r = requests.get(
+ "https://nominatim.openstreetmap.org/reverse",
+ params={
+ "lat": lat,
+ "lon": lon,
+ "format": "json",
+ "zoom": 10, # city-level
+ "addressdetails": 1
+ },
+ headers={"User-Agent": "AreWeBuriedApp/1.0"}
+ )
+ r.raise_for_status()
+ data = r.json()
+ address = data.get("address", {})
+ city = address.get("city") or address.get("town") or address.get("village") or address.get("county") or f"{lat:.2f},{lon:.2f}"
+ return city
+ except Exception as e:
+ print("Reverse geocoding error:", e)
+ return f"{lat:.2f},{lon:.2f}"
+
+@app.route("/")
+def index():
+ return render_template_string(HTML)
+
+@app.route("/api/snowfall")
+def snowfall_api():
+ lat = request.args.get("lat", type=float)
+ lon = request.args.get("lon", type=float)
+
+ if lat is None or lon is None:
+ return jsonify({"error": "Missing lat/lon"}), 400
+
+ inches, snowing, tz_name = get_today_snowfall(lat, lon)
+ today = datetime.now(pytz.timezone(tz_name)).strftime("%B %d, %Y")
+
+ city_name = get_city_name(lat, lon)
+
+ return jsonify({
+ "location": city_name,
+ "lat": lat,
+ "lon": lon,
+ "inches": inches,
+ "max_inches": MAX_SNOW_INCHES,
+ "snowing": snowing,
+ "today": today,
+ "updated": datetime.now().isoformat()
+ })
+
+
+if __name__ == "__main__":
+ app.run(host="0.0.0.0", port=5000)
diff --git a/requirements.txt b/requirements.txt
index 4287cd1..40d2675 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,5 @@
flask
requests
-gunicorn
\ No newline at end of file
+gunicorn
+pytz
+timezonefinder
diff --git a/static/main.css b/static/main.css
new file mode 100644
index 0000000..dc7fbb6
--- /dev/null
+++ b/static/main.css
@@ -0,0 +1,38 @@
+html, body {
+ margin:0; padding:0; height:100%;
+ background:#0b1320; color:#fff;
+ font-family:system-ui, sans-serif;
+ overflow:hidden;
+}
+
+.snowfall { position: fixed; inset:0; pointer-events:none; z-index:1; }
+.flake {
+ position:absolute; top:-10px; width:6px; height:6px;
+ background:rgba(255,255,255,0.9); border-radius:50%;
+ animation:fall linear infinite;
+}
+@keyframes fall { to { transform: translateY(110vh); } }
+
+.snow-wrapper {
+ position:fixed; bottom:0; left:0; width:100%; height:0%;
+ transition:height 2s ease-out; z-index:2; overflow:hidden;
+}
+svg { position:absolute; top:-80px; width:100%; height:160px; }
+.snow-body {
+ position:absolute; bottom:0; width:100%; height:100%;
+ background:
+ radial-gradient(circle at 20% 20%, #fff 0 2px, transparent 3px),
+ radial-gradient(circle at 60% 40%, #f2f8ff 0 2px, transparent 3px),
+ radial-gradient(circle at 80% 30%, #fff 0 2px, transparent 3px),
+ linear-gradient(#fff, #e6f2ff);
+ background-size:40px 40px,60px 60px,50px 50px,100% 100%;
+}
+
+.container {
+ position:relative; z-index:3; height:100%;
+ display:flex; flex-direction:column; justify-content:center;
+ align-items:center; gap:.75rem; pointer-events:none;
+}
+h1 { font-size:clamp(2.5rem,6vw,4rem); margin:0; }
+.amount { font-size:clamp(1.5rem,4vw,2.5rem); opacity:0.9; }
+.meta { opacity:0.6; font-size:0.9rem; }
diff --git a/static/main.js b/static/main.js
new file mode 100644
index 0000000..bead025
--- /dev/null
+++ b/static/main.js
@@ -0,0 +1,44 @@
+let flakesStarted = false;
+
+async function loadSnow() {
+ if (!navigator.geolocation) return alert('Geolocation not supported');
+
+ navigator.geolocation.getCurrentPosition(async (pos) => {
+ const lat = pos.coords.latitude;
+ const lon = pos.coords.longitude;
+
+ const res = await fetch(`/api/snowfall?lat=${lat}&lon=${lon}`);
+ const data = await res.json();
+
+ document.getElementById('amount').textContent = data.inches + ' inches today';
+ document.getElementById('meta').textContent = data.location + ' • ' + data.today;
+
+ const percent = Math.min(data.inches / data.max_inches, 1);
+ document.getElementById('snowWrapper').style.height = (percent * 100) + '%';
+
+ const now = new Date();
+ document.getElementById('updated').textContent =
+ 'Last updated: ' + now.toLocaleTimeString();
+
+ if (data.snowing && !flakesStarted) {
+ spawnSnowflakes();
+ flakesStarted = true;
+ }
+ });
+}
+
+function spawnSnowflakes() {
+ const container = document.getElementById('snowfall');
+ for (let i = 0; i < 90; i++) {
+ const flake = document.createElement('div');
+ flake.className = 'flake';
+ flake.style.left = Math.random()*100+'vw';
+ flake.style.animationDuration = 5+Math.random()*10+'s';
+ flake.style.opacity = Math.random();
+ flake.style.transform = `scale(${Math.random()+0.5})`;
+ container.appendChild(flake);
+ }
+}
+
+loadSnow();
+setInterval(loadSnow, 10*60*1000);
diff --git a/templates/index.html b/templates/index.html
new file mode 100644
index 0000000..f99ef8c
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,25 @@
+
+
+
+
+Are We Buried?
+
+
+
+
+
+
+
+
Are We Buried?
+
-- in
+
--
+
Last updated: --
+
+
+
+
diff --git a/utils/__init__.py b/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/utils/geocode.py b/utils/geocode.py
new file mode 100644
index 0000000..f9345d9
--- /dev/null
+++ b/utils/geocode.py
@@ -0,0 +1,17 @@
+import requests
+
+def get_city_name(lat, lon):
+ try:
+ r = requests.get(
+ "https://nominatim.openstreetmap.org/reverse",
+ params={"lat": lat, "lon": lon, "format": "json", "zoom": 10, "addressdetails": 1},
+ headers={"User-Agent": "AreWeBuriedApp/1.0"}
+ )
+ r.raise_for_status()
+ data = r.json()
+ address = data.get("address", {})
+ city = address.get("city") or address.get("town") or address.get("village") or address.get("county") or f"{lat:.2f},{lon:.2f}"
+ return city
+ except Exception as e:
+ print("Reverse geocoding error:", e)
+ return f"{lat:.2f},{lon:.2f}"
diff --git a/utils/weather.py b/utils/weather.py
new file mode 100644
index 0000000..51ddd90
--- /dev/null
+++ b/utils/weather.py
@@ -0,0 +1,43 @@
+import requests
+from datetime import datetime
+import pytz
+from timezonefinder import TimezoneFinder
+from config import OPEN_METEO_URL
+
+tf = TimezoneFinder()
+
+def get_today_snowfall(lat, lon):
+ try:
+ tz_name = tf.timezone_at(lat=lat, lng=lon) or "UTC"
+
+ params = {
+ "latitude": lat,
+ "longitude": lon,
+ "hourly": "snowfall",
+ "timezone": tz_name
+ }
+
+ r = requests.get(OPEN_METEO_URL, params=params, timeout=5)
+ r.raise_for_status()
+ data = r.json()
+
+ hourly = data.get("hourly", {})
+ times = hourly.get("time", [])
+ snow_values = hourly.get("snowfall", [])
+
+ today_str = datetime.now(pytz.timezone(tz_name)).date().isoformat()
+
+ snowfall_cm = 0.0
+ snowing_now = False
+ for t, s in zip(times, snow_values):
+ if t.startswith(today_str):
+ snowfall_cm += s
+ if s > 0:
+ snowing_now = True
+
+ inches = round(snowfall_cm / 2.54, 1)
+ return inches, snowing_now, tz_name
+
+ except Exception as e:
+ print("Error fetching snowfall:", e)
+ return 0.0, False, "UTC"