diff --git a/CLAUDE.md b/CLAUDE.md index 24797fc..6f52788 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,13 +4,22 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Running the Server +Pipekit runs as a systemd service (`pipekit.service`). Common commands: + +```bash +sudo systemctl status pipekit # check status +sudo systemctl restart pipekit # restart after changes +sudo systemctl stop pipekit # stop +sudo journalctl -u pipekit -f # follow logs +``` + +For dev/testing outside the service — never nohup or background it: + ```bash pipekit serve # default host/port from config.yaml pipekit serve --host 0.0.0.0 --port 8080 --reload # dev mode with auto-reload ``` -The user runs the server themselves in their own terminal — do not nohup or background it. - ## Other CLI Commands ```bash diff --git a/pipekit/scheduler.py b/pipekit/scheduler.py index 64b9727..a29f941 100644 --- a/pipekit/scheduler.py +++ b/pipekit/scheduler.py @@ -17,8 +17,8 @@ from datetime import datetime, timedelta, timezone log = logging.getLogger(__name__) -def _utcnow() -> datetime: - return datetime.now(timezone.utc) +def _now() -> datetime: + return datetime.now() def _check_and_fire() -> None: @@ -29,7 +29,7 @@ def _check_and_fire() -> None: log.error("scheduler: failed to load schedules: %s", e) return - now = _utcnow() + now = _now() for sched in schedules: try: _maybe_fire(sched, now) @@ -43,15 +43,15 @@ def _maybe_fire(sched: dict, now: datetime) -> None: last_str = sched["last_fired_at"] if last_str: - last_dt = datetime.fromisoformat(last_str).replace(tzinfo=timezone.utc) + # SQLite stores UTC; convert to local for cron evaluation. + last_dt = datetime.fromisoformat(last_str).replace( + tzinfo=timezone.utc).astimezone().replace(tzinfo=None) else: - # Never fired — look back 2 minutes so we pick up the current tick - # without replaying indefinitely old missed ticks. last_dt = now - timedelta(seconds=120) try: cron = croniter(sched["cron_expr"], last_dt) - next_dt = cron.get_next(datetime).replace(tzinfo=timezone.utc) + next_dt = cron.get_next(datetime) except CroniterBadCronError: log.warning("scheduler: invalid cron_expr %r for schedule id=%s — skipping", sched["cron_expr"], sched["id"]) diff --git a/pipekit/web/app.py b/pipekit/web/app.py index 19f4962..2132c65 100644 --- a/pipekit/web/app.py +++ b/pipekit/web/app.py @@ -38,7 +38,20 @@ def _fmt_duration(seconds) -> str: return f"{s // 60}m {s % 60:02d}s" +def _fmt_localtime(utc_str) -> str: + """Convert a UTC datetime string from SQLite to local time for display.""" + if not utc_str: + return "—" + from datetime import datetime, timezone + try: + dt = datetime.fromisoformat(str(utc_str)).replace(tzinfo=timezone.utc) + return dt.astimezone().strftime("%Y-%m-%d %H:%M %Z") + except (ValueError, TypeError): + return str(utc_str) + + _templates.env.filters["duration"] = _fmt_duration +_templates.env.filters["localtime"] = _fmt_localtime def mount_web(app: FastAPI) -> None: @@ -656,13 +669,13 @@ def _schedules_with_next(schedules: list[dict]) -> list[dict]: """Attach a 'next_fire_at' string to each schedule dict.""" from croniter import croniter, CroniterBadCronError from datetime import datetime, timezone - now = datetime.now(timezone.utc) + now = datetime.now() result = [] for s in schedules: s = dict(s) try: cron = croniter(s["cron_expr"], now) - s["next_fire_at"] = cron.get_next(datetime).strftime("%Y-%m-%d %H:%M UTC") + s["next_fire_at"] = cron.get_next(datetime).strftime("%Y-%m-%d %H:%M %Z") except CroniterBadCronError: s["next_fire_at"] = "invalid expression" result.append(s) diff --git a/pipekit/web/templates/_group_live.html b/pipekit/web/templates/_group_live.html index c95aab5..3216c49 100644 --- a/pipekit/web/templates/_group_live.html +++ b/pipekit/web/templates/_group_live.html @@ -28,7 +28,7 @@ {% for r in recent_runs %} #{{ r.id }} - {{ r.started_at or "—" }} + {{ r.started_at | localtime }} {{ r.duration_s | duration }} {{ r.triggered_by or "—" }} {{ r.status }} diff --git a/pipekit/web/templates/_group_run_live.html b/pipekit/web/templates/_group_run_live.html index 42768b9..3eb1105 100644 --- a/pipekit/web/templates/_group_run_live.html +++ b/pipekit/web/templates/_group_run_live.html @@ -31,7 +31,7 @@ #{{ r.id }} {{ r.module_name }} - {{ r.started_at or "—" }} + {{ r.started_at | localtime }} {{ r.duration_s | duration }} {{ r.status }} {{ r.row_count if r.row_count is not none else "—" }} diff --git a/pipekit/web/templates/_module_live.html b/pipekit/web/templates/_module_live.html index 47bd24e..821ce36 100644 --- a/pipekit/web/templates/_module_live.html +++ b/pipekit/web/templates/_module_live.html @@ -21,7 +21,7 @@ {% for r in recent_runs %} #{{ r.id }} - {{ r.started_at or '—' }} + {{ r.started_at | localtime }} {{ r.duration_s | duration }} {{ r.status }} {{ r.row_count if r.row_count is not none else "—" }} diff --git a/pipekit/web/templates/group_detail.html b/pipekit/web/templates/group_detail.html index f91482b..777c0bf 100644 --- a/pipekit/web/templates/group_detail.html +++ b/pipekit/web/templates/group_detail.html @@ -67,7 +67,7 @@ cron expression - next fire (UTC) + next fire last fired enabled @@ -77,7 +77,7 @@ {{ s.cron_expr }} {% if s.enabled %}{{ s.next_fire_at }}{% else %}—{% endif %} - {{ s.last_fired_at or "never" }} + {{ s.last_fired_at | localtime }} {% if s.enabled %} yes diff --git a/pipekit/web/templates/group_form.html b/pipekit/web/templates/group_form.html index 123f8ed..3cd1b87 100644 --- a/pipekit/web/templates/group_form.html +++ b/pipekit/web/templates/group_form.html @@ -66,7 +66,7 @@
Schedules - cron expressions in UTC — e.g. 0 4 * * * for daily at 04:00 + cron expressions in local time — e.g. 0 4 * * * for daily at 04:00
@@ -106,6 +106,25 @@ {% if not schedules %}
No schedules — group runs manually only.
{% endif %} +
+ cron expression reference + + + + + + + + + + + + +
expressionmeaning
0 * * * *every hour (on the hour)
*/15 * * * *every 15 minutes
0 4 * * *daily at 04:00
0 4 * * 1-5weekdays at 04:00
0 4 * * 6,0weekends at 04:00
0 0 * * 1every Monday at midnight
0 6,18 * * *twice daily at 06:00 and 18:00
0 0 1 * *first day of each month
+
+ Format: minute hour day-of-month month day-of-week  ·  all times local +
+
diff --git a/pipekit/web/templates/group_run_detail.html b/pipekit/web/templates/group_run_detail.html index 1abe76d..a758abd 100644 --- a/pipekit/web/templates/group_run_detail.html +++ b/pipekit/web/templates/group_run_detail.html @@ -8,7 +8,7 @@ Group run #{{ group_run.id }} {{ group_run.group_name }} · - started {{ group_run.started_at }} + started {{ group_run.started_at | localtime }} {{ group_run.status }} @@ -16,8 +16,8 @@
-
started
{{ group_run.started_at }}
-
finished
{{ group_run.finished_at or "—" }}
+
started
{{ group_run.started_at | localtime }}
+
finished
{{ group_run.finished_at | localtime }}
duration
{{ group_run.duration_s | duration }}
triggered by
{{ group_run.triggered_by or "—" }}
@@ -50,7 +50,7 @@ #{{ r.id }} {{ r.module_name }} - {{ r.started_at or "—" }} + {{ r.started_at | localtime }} {{ r.duration_s | duration }} {{ r.status }} {{ r.row_count if r.row_count is not none else "—" }} diff --git a/pipekit/web/templates/groups.html b/pipekit/web/templates/groups.html index 995bbdd..007d4a6 100644 --- a/pipekit/web/templates/groups.html +++ b/pipekit/web/templates/groups.html @@ -28,7 +28,7 @@ {{ g.name }} {{ g.member_count }} - {{ g.last_run_at or "—" }} + {{ g.last_run_at | localtime }} {% if g.last_status %} {{ g.last_status }} diff --git a/pipekit/web/templates/modules_index.html b/pipekit/web/templates/modules_index.html index 9d03972..3435fa6 100644 --- a/pipekit/web/templates/modules_index.html +++ b/pipekit/web/templates/modules_index.html @@ -39,7 +39,7 @@ {{ g.group_name }} {% endfor %} - {{ m.last_run_at or "—" }} + {{ m.last_run_at | localtime }} {% with module=m %}{% include "_module_status_pill.html" %}{% endwith %} {{ m.last_row_count if m.last_row_count is not none else "—" }} diff --git a/pipekit/web/templates/run_detail.html b/pipekit/web/templates/run_detail.html index 7c901d0..5c64b1f 100644 --- a/pipekit/web/templates/run_detail.html +++ b/pipekit/web/templates/run_detail.html @@ -8,14 +8,14 @@ Run #{{ run.id }} {{ run.module_name }} · - started {{ run.started_at }} + started {{ run.started_at | localtime }} {{ run.status }}
-
started
{{ run.started_at }}
-
finished
{{ run.finished_at or '—' }}
+
started
{{ run.started_at | localtime }}
+
finished
{{ run.finished_at | localtime }}
rows
{{ run.row_count if run.row_count is not none else '—' }}
watermarks
{{ run.watermark_values_json or '—' }}
{% if run.error %}
error
{{ run.error }}
{% endif %} diff --git a/pipekit/web/templates/runs.html b/pipekit/web/templates/runs.html index 102e840..c3ad514 100644 --- a/pipekit/web/templates/runs.html +++ b/pipekit/web/templates/runs.html @@ -33,7 +33,7 @@ #{{ r.id }} {{ r.module_name }} - {{ r.started_at or '—' }} + {{ r.started_at | localtime }} {{ r.duration_s | duration }} {{ r.status }} {{ r.row_count if r.row_count is not none else "—" }}