Skip to content

Commit 33fd79e

Browse files
committed
refactor
1 parent ef89d73 commit 33fd79e

17 files changed

Lines changed: 907 additions & 774 deletions

Dockerfile

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,7 @@ USER tracekit
3131

3232
EXPOSE 5000
3333

34-
ENV FLASK_ENV=development
35-
ENV FLASK_DEBUG=1
36-
3734
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s CMD curl -f http://127.0.0.1:5000/health || exit 1
3835

3936
ENTRYPOINT ["/app/docker-entrypoint.sh"]
40-
CMD ["python", "app/main.py"]
37+
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "--timeout", "120", "app.main:app"]

app/calendar_data.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
"""Calendar data helpers for the tracekit web app."""
2+
3+
import calendar as _cal
4+
from datetime import UTC, datetime
5+
from typing import Any
6+
7+
from db_init import _init_db
8+
from helpers import get_current_date_in_timezone
9+
10+
11+
def get_sync_calendar_data(config: dict[str, Any]) -> dict[str, Any]:
12+
"""Compatibility shim — returns full calendar data (used by tests).
13+
14+
In production the page uses get_calendar_shell + get_single_month_data
15+
so that each month loads independently. This function still works for
16+
the test suite which imports it directly.
17+
"""
18+
shell = get_calendar_shell(config)
19+
if shell.get("error"):
20+
return shell
21+
22+
months_with_data = []
23+
for stub in shell["months"]:
24+
month_data = get_single_month_data(config, stub["year_month"])
25+
if month_data.get("error"):
26+
months_with_data.append(stub)
27+
else:
28+
months_with_data.append(month_data)
29+
30+
return {
31+
"months": months_with_data,
32+
"providers": shell["providers"],
33+
"date_range": shell["date_range"],
34+
"total_months": shell["total_months"],
35+
}
36+
37+
38+
def get_calendar_shell(config: dict[str, Any] | None = None) -> dict[str, Any]:
39+
"""Return month stubs and providers list — no activity table scans."""
40+
if not _init_db():
41+
return {"error": "Database not available"}
42+
43+
try:
44+
from tracekit.db import get_db
45+
from tracekit.provider_sync import ProviderSync
46+
47+
db = get_db()
48+
db.connect(reuse_if_open=True)
49+
50+
rows = ProviderSync.select(ProviderSync.year_month, ProviderSync.provider).order_by(
51+
ProviderSync.year_month, ProviderSync.provider
52+
)
53+
records = [(r.year_month, r.provider) for r in rows]
54+
55+
if not records:
56+
return {"months": [], "providers": [], "date_range": (None, None), "total_months": 0}
57+
58+
year_months_all = [r[0] for r in records]
59+
date_range = (min(year_months_all), max(year_months_all))
60+
providers = sorted({r[1] for r in records})
61+
62+
start_year, start_month = map(int, date_range[0].split("-"))
63+
end_year, end_month = map(int, date_range[1].split("-"))
64+
65+
current_date = get_current_date_in_timezone(config)
66+
current_ym = f"{current_date.year:04d}-{current_date.month:02d}"
67+
if current_ym > date_range[1]:
68+
end_year, end_month = current_date.year, current_date.month
69+
70+
all_months = []
71+
year, month = start_year, start_month
72+
while year < end_year or (year == end_year and month <= end_month):
73+
ym = f"{year:04d}-{month:02d}"
74+
all_months.append(
75+
{
76+
"year_month": ym,
77+
"year": year,
78+
"month": month,
79+
"month_name": datetime(year, month, 1).strftime("%B"),
80+
}
81+
)
82+
month += 1
83+
if month > 12:
84+
month = 1
85+
year += 1
86+
87+
return {
88+
"months": all_months,
89+
"providers": providers,
90+
"date_range": date_range,
91+
"total_months": len(all_months),
92+
}
93+
except Exception as e:
94+
return {"error": f"Database error: {e}"}
95+
96+
97+
def get_single_month_data(config: dict[str, Any] | None, year_month: str) -> dict[str, Any]:
98+
"""Return sync status and activity counts for one month.
99+
100+
Activity queries are scoped to the month's timestamp range so this is
101+
fast even for large databases.
102+
"""
103+
if not _init_db():
104+
return {"error": "Database not available"}
105+
106+
try:
107+
from tracekit.db import get_db
108+
from tracekit.provider_sync import ProviderSync
109+
from tracekit.providers.file.file_activity import FileActivity
110+
from tracekit.providers.garmin.garmin_activity import GarminActivity
111+
from tracekit.providers.ridewithgps.ridewithgps_activity import RideWithGPSActivity
112+
from tracekit.providers.spreadsheet.spreadsheet_activity import SpreadsheetActivity
113+
from tracekit.providers.strava.strava_activity import StravaActivity
114+
from tracekit.providers.stravajson.stravajson_activity import StravaJsonActivity
115+
116+
db = get_db()
117+
db.connect(reuse_if_open=True)
118+
119+
synced_rows = ProviderSync.select(ProviderSync.provider).where(ProviderSync.year_month == year_month)
120+
synced_providers = [r.provider for r in synced_rows]
121+
122+
all_rows = ProviderSync.select(ProviderSync.provider).distinct()
123+
providers = sorted({r.provider for r in all_rows})
124+
125+
provider_status = {p: p in synced_providers for p in providers}
126+
127+
year_int, month_int = map(int, year_month.split("-"))
128+
start_ts = int(datetime(year_int, month_int, 1, tzinfo=UTC).timestamp())
129+
last_day = _cal.monthrange(year_int, month_int)[1]
130+
end_ts = int(datetime(year_int, month_int, last_day, 23, 59, 59, tzinfo=UTC).timestamp())
131+
132+
provider_models = {
133+
"strava": StravaActivity,
134+
"garmin": GarminActivity,
135+
"ridewithgps": RideWithGPSActivity,
136+
"spreadsheet": SpreadsheetActivity,
137+
"file": FileActivity,
138+
"stravajson": StravaJsonActivity,
139+
}
140+
141+
activity_counts: dict[str, int] = {}
142+
for provider, model in provider_models.items():
143+
try:
144+
count = (
145+
model.select()
146+
.where(
147+
model.start_time.is_null(False) & (model.start_time >= start_ts) & (model.start_time <= end_ts)
148+
)
149+
.count()
150+
)
151+
if count > 0:
152+
activity_counts[provider] = count
153+
except Exception as e:
154+
print(f"Error counting {provider} activities for {year_month}: {e}")
155+
156+
total_activities = sum(activity_counts.values())
157+
158+
return {
159+
"year_month": year_month,
160+
"year": year_int,
161+
"month": month_int,
162+
"month_name": datetime(year_int, month_int, 1).strftime("%B"),
163+
"providers": providers,
164+
"synced_providers": synced_providers,
165+
"provider_status": provider_status,
166+
"activity_counts": activity_counts,
167+
"total_activities": total_activities,
168+
}
169+
except Exception as e:
170+
return {"error": f"Database error: {e}"}

app/db_init.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Database initialisation and config loading for the tracekit web app."""
2+
3+
from typing import Any
4+
5+
_db_initialized = False
6+
7+
8+
def _init_db() -> bool:
9+
"""Configure the DB and ensure all tables exist.
10+
11+
Resolution order (no config file needed):
12+
1. DATABASE_URL env var → PostgreSQL
13+
2. METADATA_DB env var → SQLite at that path
14+
3. Default → metadata.sqlite3 in cwd
15+
"""
16+
global _db_initialized
17+
if not _db_initialized:
18+
try:
19+
from tracekit.appconfig import get_db_path_from_env
20+
from tracekit.database import get_all_models, migrate_tables
21+
from tracekit.db import configure_db
22+
23+
configure_db(get_db_path_from_env())
24+
migrate_tables(get_all_models())
25+
_db_initialized = True
26+
except Exception as e:
27+
print(f"DB init failed: {e}")
28+
return False
29+
return True
30+
31+
32+
def load_tracekit_config() -> dict[str, Any]:
33+
"""Load tracekit config — always returns a valid dict.
34+
35+
Priority: DB rows → JSON file (migrated in on first call) → built-in defaults.
36+
Never returns an error dict; the app always has a working config.
37+
"""
38+
_init_db()
39+
from tracekit.appconfig import load_config
40+
41+
return load_config()

app/helpers.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""Shared helper functions for the tracekit web app."""
2+
3+
from datetime import UTC, datetime
4+
from typing import Any
5+
6+
import pytz
7+
from db_init import _init_db
8+
9+
10+
def get_current_date_in_timezone(config: dict[str, Any]):
11+
"""Get the current date in the configured timezone."""
12+
try:
13+
timezone_str = config.get("home_timezone", "UTC")
14+
tz = pytz.timezone(timezone_str)
15+
now = datetime.now(tz)
16+
return now.date()
17+
except Exception:
18+
return datetime.now(pytz.UTC).date()
19+
20+
21+
def get_database_info(config: dict[str, Any] | None = None) -> dict[str, Any]:
22+
"""Get basic information about the configured database."""
23+
if not _init_db():
24+
return {"error": "Database not available"}
25+
26+
try:
27+
from tracekit.database import get_all_models
28+
from tracekit.db import get_db
29+
30+
db = get_db()
31+
db.connect(reuse_if_open=True)
32+
33+
models = get_all_models()
34+
table_counts = {}
35+
for model in models:
36+
table_name = model._meta.table_name
37+
table_counts[table_name] = model.select().count()
38+
39+
return {
40+
"tables": table_counts,
41+
"total_tables": len(table_counts),
42+
}
43+
except Exception as e:
44+
return {"error": f"Database error: {e}"}
45+
46+
47+
def get_most_recent_activity(config: dict[str, Any] | None = None) -> dict[str, Any]:
48+
"""Return the timestamp and timezone-formatted datetime of the most recent activity."""
49+
if not _init_db():
50+
return {"error": "Database not available"}
51+
52+
try:
53+
from tracekit.providers.file.file_activity import FileActivity
54+
from tracekit.providers.garmin.garmin_activity import GarminActivity
55+
from tracekit.providers.ridewithgps.ridewithgps_activity import RideWithGPSActivity
56+
from tracekit.providers.spreadsheet.spreadsheet_activity import SpreadsheetActivity
57+
from tracekit.providers.strava.strava_activity import StravaActivity
58+
from tracekit.providers.stravajson.stravajson_activity import StravaJsonActivity
59+
60+
models = [
61+
StravaActivity,
62+
GarminActivity,
63+
RideWithGPSActivity,
64+
SpreadsheetActivity,
65+
FileActivity,
66+
StravaJsonActivity,
67+
]
68+
69+
max_ts: int | None = None
70+
for model in models:
71+
try:
72+
row = (
73+
model.select(model.start_time)
74+
.where(model.start_time.is_null(False))
75+
.order_by(model.start_time.desc())
76+
.first()
77+
)
78+
if row and row.start_time:
79+
ts = int(row.start_time)
80+
if max_ts is None or ts > max_ts:
81+
max_ts = ts
82+
except Exception:
83+
pass
84+
85+
if max_ts is None:
86+
return {"timestamp": None, "formatted": None}
87+
88+
tz_str = (config or {}).get("home_timezone", "UTC")
89+
try:
90+
tz = pytz.timezone(tz_str)
91+
except Exception:
92+
tz = pytz.UTC
93+
94+
dt = datetime.fromtimestamp(max_ts, tz=UTC).astimezone(tz)
95+
formatted = dt.strftime("%-d %b %Y, %H:%M %Z")
96+
return {"timestamp": max_ts, "formatted": formatted}
97+
except Exception as e:
98+
return {"error": f"Database error: {e}"}
99+
100+
101+
def sort_providers(providers: dict[str, Any]) -> list[tuple[str, dict[str, Any]]]:
102+
"""Sort providers by priority (lowest first) with disabled providers at the end."""
103+
enabled: list[tuple[int, str, dict[str, Any]]] = []
104+
disabled: list[tuple[str, dict[str, Any]]] = []
105+
for name, cfg in providers.items():
106+
if cfg.get("enabled", False):
107+
enabled.append((cfg.get("priority", 999), name, cfg))
108+
else:
109+
disabled.append((name, cfg))
110+
enabled.sort(key=lambda x: x[0])
111+
result = [(name, cfg) for _, name, cfg in enabled]
112+
result.extend(disabled)
113+
return result

0 commit comments

Comments
 (0)