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/deploy.sh b/deploy.sh index d94df9b..2e1005a 100755 --- a/deploy.sh +++ b/deploy.sh @@ -182,8 +182,31 @@ step "Systemd unit" if [ ! -f "$UNIT_SRC" ]; then echo " WARNING: $UNIT_SRC not found — skipping" else + # Detect JAVA_HOME — check PATH first, then search each location individually + JAVA_BIN="$(command -v java 2>/dev/null || true)" + if [ -z "$JAVA_BIN" ]; then + for _dir in /opt /usr/lib/jvm /usr/local/lib/jvm; do + [ -d "$_dir" ] || continue + JAVA_BIN="$(find "$_dir" -maxdepth 3 -name java -type f 2>/dev/null | head -1 || true)" + [ -n "$JAVA_BIN" ] && break + done + fi + if [ -z "$JAVA_BIN" ]; then + echo " WARNING: java not found — JAVA_HOME will not be set in unit" + JAVA_HOME_DETECTED="" + else + JAVA_HOME_DETECTED="$(dirname "$(dirname "$(readlink -f "$JAVA_BIN")")")" + echo " Detected JAVA_HOME: $JAVA_HOME_DETECTED" + fi + echo " Installing $UNIT_DST" - cp "$UNIT_SRC" "$UNIT_DST" + if [ -n "$JAVA_HOME_DETECTED" ]; then + # Inject Environment lines after WorkingDirectory= using printf to handle newlines portably + ENV_BLOCK="$(printf 'Environment=JAVA_HOME=%s\nEnvironment=PATH=%s/bin:/usr/local/bin:/usr/bin:/bin' "$JAVA_HOME_DETECTED" "$JAVA_HOME_DETECTED")" + awk -v env="$ENV_BLOCK" '/^WorkingDirectory=/{print; print env; next}1' "$UNIT_SRC" > "$UNIT_DST" + else + cp "$UNIT_SRC" "$UNIT_DST" + fi echo " Running systemctl daemon-reload" systemctl daemon-reload echo " Enabling $SERVICE_NAME service" diff --git a/pipekit/repo.py b/pipekit/repo.py index b2ef192..da54df7 100644 --- a/pipekit/repo.py +++ b/pipekit/repo.py @@ -662,7 +662,10 @@ def create_group_run(group_id: int, *, triggered_by: str | None = None) -> int: def get_group_run(group_run_id: int) -> dict | None: with db.connect() as c: return _row(c.execute( - "SELECT gr.*, g.name AS group_name " + "SELECT gr.*, g.name AS group_name, " + "CASE WHEN gr.started_at IS NOT NULL AND gr.finished_at IS NOT NULL " + "THEN CAST(ROUND((julianday(gr.finished_at) - julianday(gr.started_at)) * 86400) AS INTEGER) " + "ELSE NULL END AS duration_s " "FROM group_run gr " "JOIN grp g ON gr.group_id=g.id " "WHERE gr.id=?", 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 %}
0 4 * * * for daily at 04:00
+ cron expressions in local time — e.g. 0 4 * * * for daily at 04:00
| expression | meaning |
|---|---|
| 0 * * * * | every hour (on the hour) |
| */15 * * * * | every 15 minutes |
| 0 4 * * * | daily at 04:00 |
| 0 4 * * 1-5 | weekdays at 04:00 |
| 0 4 * * 6,0 | weekends at 04:00 |
| 0 0 * * 1 | every Monday at midnight |
| 0 6,18 * * * | twice daily at 06:00 and 18:00 |
| 0 0 1 * * | first day of each month |
minute hour day-of-month month day-of-week · all times local
+