tz: server-wide display timezone via TZ env var (default EST/EDT)
User-reported issue: server logs were timestamped in UTC ("05:36:20"
when local was ~01:36 EDT), and the PDF report's "Created" footer
similarly showed raw UTC. Inconsistent with the modal which already
converts to browser local via toLocaleString.
Solution: standard Linux TZ env var. Set once in the container, and:
- Python's datetime.now() uses local
- Logging module's timestamps use local
- matplotlib renderers + report_pdf formatters use local
- astimezone() conversions resolve to the configured TZ
DB columns stay UTC (created_at uses SQLite's strftime('%Y-...Z', 'now')
which is always UTC, regardless of TZ env var — proper "store UTC,
display local" pattern).
Changes:
- Dockerfile: install tzdata (python:3.11-slim omits the timezone
database), set default TZ=America/New_York
- sfm/report_pdf.py: _fmt_iso_to_bw and _split_iso_to_date_time now
convert UTC inputs (Z-suffixed) to local via astimezone(); naïve
inputs (BW recorded-at, already unit-local) returned as-is.
New _to_display_local helper centralizes the logic.
- "Created" line in the PDF page footer now uses the converted
timestamp.
Override per-deployment via the TZ env var in docker-compose
(separate commit on terra-view side).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+12
-1
@@ -2,10 +2,21 @@ FROM python:3.11-slim
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# tzdata is required for the TZ env var to take effect (python:slim
|
||||||
|
# omits the timezone database). Without it, datetime.now() / logging
|
||||||
|
# / matplotlib all stay in UTC regardless of TZ. Default zone gets
|
||||||
|
# set further down via ENV; users override per-deployment via the
|
||||||
|
# `TZ` env var in docker-compose.
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends curl && \
|
apt-get install -y --no-install-recommends curl tzdata && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Default display timezone — applied to server logs, datetime.now(),
|
||||||
|
# matplotlib rendered timestamps, and any naïve-vs-aware datetime
|
||||||
|
# conversions in the PDF renderer. Override via TZ env var in
|
||||||
|
# docker-compose; storage in the DB is always UTC regardless.
|
||||||
|
ENV TZ=America/New_York
|
||||||
|
|
||||||
COPY pyproject.toml requirements.txt ./
|
COPY pyproject.toml requirements.txt ./
|
||||||
COPY minimateplus ./minimateplus
|
COPY minimateplus ./minimateplus
|
||||||
COPY micromate ./micromate
|
COPY micromate ./micromate
|
||||||
|
|||||||
+34
-13
@@ -348,9 +348,11 @@ def render_event_report_pdf(rd: ReportData) -> bytes:
|
|||||||
# Page footer (common to both layouts) — Created date + event id.
|
# Page footer (common to both layouts) — Created date + event id.
|
||||||
# Pushed to the very page bottom so it doesn't collide with the
|
# Pushed to the very page bottom so it doesn't collide with the
|
||||||
# waveform footer scale / trigger legend lines just above.
|
# waveform footer scale / trigger legend lines just above.
|
||||||
|
# Convert UTC server_received_at to local for display.
|
||||||
|
created_local = _fmt_iso_to_bw(rd.server_received_at) if rd.server_received_at else "—"
|
||||||
fig.text(
|
fig.text(
|
||||||
0.07, 0.005,
|
0.07, 0.005,
|
||||||
f"Created: {rd.server_received_at or '—'} • seismo-relay",
|
f"Created: {created_local} • seismo-relay",
|
||||||
fontsize=6, color="#888", ha="left",
|
fontsize=6, color="#888", ha="left",
|
||||||
)
|
)
|
||||||
fig.text(
|
fig.text(
|
||||||
@@ -419,31 +421,50 @@ def _render_histogram_layout(fig, rd: ReportData) -> None:
|
|||||||
_draw_histogram_subplot(fig, gs[3], rd)
|
_draw_histogram_subplot(fig, gs[3], rd)
|
||||||
|
|
||||||
|
|
||||||
|
def _to_display_local(iso: str):
|
||||||
|
"""Parse an ISO timestamp and return a datetime in the system's local
|
||||||
|
timezone (set by the TZ env var, default America/New_York via the
|
||||||
|
Dockerfile).
|
||||||
|
|
||||||
|
Behaviour:
|
||||||
|
- "...Z" or "...+HH:MM" suffix → tz-aware UTC → converted to local
|
||||||
|
- Naïve "YYYY-MM-DDTHH:MM:SS" (no tz) → returned as-is. This
|
||||||
|
matches the convention used elsewhere in seismo-relay: BW's
|
||||||
|
recorded-at timestamps are naïve and ALREADY in the unit's
|
||||||
|
local clock; we don't second-guess them.
|
||||||
|
"""
|
||||||
|
import datetime as _dt
|
||||||
|
dt = _dt.datetime.fromisoformat(iso.replace("Z", "+00:00"))
|
||||||
|
if dt.tzinfo is not None:
|
||||||
|
# Convert from UTC (or other tz) → local per the TZ env var.
|
||||||
|
# astimezone() without arg uses the system timezone.
|
||||||
|
dt = dt.astimezone()
|
||||||
|
return dt
|
||||||
|
|
||||||
|
|
||||||
def _fmt_iso_to_bw(iso: Optional[str]) -> Optional[str]:
|
def _fmt_iso_to_bw(iso: Optional[str]) -> Optional[str]:
|
||||||
"""Convert a ISO-8601 timestamp like '2026-05-16T22:30:37' to BW's
|
"""Convert an ISO-8601 timestamp to BW's display format
|
||||||
display format '22:30:37 May 16, 2026'. Returns input unchanged if
|
'22:30:37 May 16, 2026'. UTC inputs (with Z suffix) are
|
||||||
it doesn't look like ISO."""
|
converted to the system's local timezone first; naïve inputs
|
||||||
|
are formatted as-is. Returns input unchanged on parse failure."""
|
||||||
if not iso or "T" not in iso:
|
if not iso or "T" not in iso:
|
||||||
return iso
|
return iso
|
||||||
try:
|
try:
|
||||||
import datetime as _dt
|
return _to_display_local(iso).strftime("%H:%M:%S %B %d, %Y").replace(" 0", " ")
|
||||||
dt = _dt.datetime.fromisoformat(iso.replace("Z", "+00:00"))
|
|
||||||
return dt.strftime("%H:%M:%S %B %d, %Y").replace(" 0", " ")
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return iso
|
return iso
|
||||||
|
|
||||||
|
|
||||||
def _split_iso_to_date_time(iso: Optional[str]) -> tuple[Optional[str], Optional[str]]:
|
def _split_iso_to_date_time(iso: Optional[str]) -> tuple[Optional[str], Optional[str]]:
|
||||||
"""Split an ISO timestamp into BW-formatted ("May 27 /26", "06:06:14")
|
"""Split an ISO timestamp into BW-formatted ('May 27 /26', '06:06:14')
|
||||||
date+time strings. Used for the histogram stats table where the
|
date+time strings. Used for the histogram stats table where the
|
||||||
Date and Time rows are presented separately. Returns (None, None)
|
Date and Time rows are presented separately. UTC inputs are
|
||||||
if the input isn't a valid ISO datetime."""
|
converted to local time first. Returns (None, None) on parse failure."""
|
||||||
if not iso:
|
if not iso:
|
||||||
return (None, None)
|
return (None, None)
|
||||||
try:
|
try:
|
||||||
import datetime as _dt
|
dt = _to_display_local(iso)
|
||||||
dt = _dt.datetime.fromisoformat(iso.replace("Z", "+00:00"))
|
# BW format: 'May 27 /26' (3-letter month + 2-digit year)
|
||||||
# BW format: "May 27 /26" (3-letter month + 2-digit year)
|
|
||||||
date_str = dt.strftime("%b %d /%y").replace(" 0", " ")
|
date_str = dt.strftime("%b %d /%y").replace(" 0", " ")
|
||||||
time_str = dt.strftime("%H:%M:%S")
|
time_str = dt.strftime("%H:%M:%S")
|
||||||
return (date_str, time_str)
|
return (date_str, time_str)
|
||||||
|
|||||||
Reference in New Issue
Block a user