Merge branch 'localtime'
- Convert all timestamps to local time for display and scheduling - deploy.sh: detect JAVA_HOME and inject into systemd unit at deploy time - repo: add duration_s to get_group_run query
This commit is contained in:
commit
a6cc8da83f
13
CLAUDE.md
13
CLAUDE.md
@ -4,13 +4,22 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Running the Server
|
## 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
|
```bash
|
||||||
pipekit serve # default host/port from config.yaml
|
pipekit serve # default host/port from config.yaml
|
||||||
pipekit serve --host 0.0.0.0 --port 8080 --reload # dev mode with auto-reload
|
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
|
## Other CLI Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
23
deploy.sh
23
deploy.sh
@ -182,8 +182,31 @@ step "Systemd unit"
|
|||||||
if [ ! -f "$UNIT_SRC" ]; then
|
if [ ! -f "$UNIT_SRC" ]; then
|
||||||
echo " WARNING: $UNIT_SRC not found — skipping"
|
echo " WARNING: $UNIT_SRC not found — skipping"
|
||||||
else
|
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"
|
echo " Installing $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"
|
cp "$UNIT_SRC" "$UNIT_DST"
|
||||||
|
fi
|
||||||
echo " Running systemctl daemon-reload"
|
echo " Running systemctl daemon-reload"
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
echo " Enabling $SERVICE_NAME service"
|
echo " Enabling $SERVICE_NAME service"
|
||||||
|
|||||||
@ -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:
|
def get_group_run(group_run_id: int) -> dict | None:
|
||||||
with db.connect() as c:
|
with db.connect() as c:
|
||||||
return _row(c.execute(
|
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 "
|
"FROM group_run gr "
|
||||||
"JOIN grp g ON gr.group_id=g.id "
|
"JOIN grp g ON gr.group_id=g.id "
|
||||||
"WHERE gr.id=?",
|
"WHERE gr.id=?",
|
||||||
|
|||||||
@ -17,8 +17,8 @@ from datetime import datetime, timedelta, timezone
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _utcnow() -> datetime:
|
def _now() -> datetime:
|
||||||
return datetime.now(timezone.utc)
|
return datetime.now()
|
||||||
|
|
||||||
|
|
||||||
def _check_and_fire() -> None:
|
def _check_and_fire() -> None:
|
||||||
@ -29,7 +29,7 @@ def _check_and_fire() -> None:
|
|||||||
log.error("scheduler: failed to load schedules: %s", e)
|
log.error("scheduler: failed to load schedules: %s", e)
|
||||||
return
|
return
|
||||||
|
|
||||||
now = _utcnow()
|
now = _now()
|
||||||
for sched in schedules:
|
for sched in schedules:
|
||||||
try:
|
try:
|
||||||
_maybe_fire(sched, now)
|
_maybe_fire(sched, now)
|
||||||
@ -43,15 +43,15 @@ def _maybe_fire(sched: dict, now: datetime) -> None:
|
|||||||
|
|
||||||
last_str = sched["last_fired_at"]
|
last_str = sched["last_fired_at"]
|
||||||
if last_str:
|
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:
|
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)
|
last_dt = now - timedelta(seconds=120)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cron = croniter(sched["cron_expr"], last_dt)
|
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:
|
except CroniterBadCronError:
|
||||||
log.warning("scheduler: invalid cron_expr %r for schedule id=%s — skipping",
|
log.warning("scheduler: invalid cron_expr %r for schedule id=%s — skipping",
|
||||||
sched["cron_expr"], sched["id"])
|
sched["cron_expr"], sched["id"])
|
||||||
|
|||||||
@ -38,7 +38,20 @@ def _fmt_duration(seconds) -> str:
|
|||||||
return f"{s // 60}m {s % 60:02d}s"
|
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["duration"] = _fmt_duration
|
||||||
|
_templates.env.filters["localtime"] = _fmt_localtime
|
||||||
|
|
||||||
|
|
||||||
def mount_web(app: FastAPI) -> None:
|
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."""
|
"""Attach a 'next_fire_at' string to each schedule dict."""
|
||||||
from croniter import croniter, CroniterBadCronError
|
from croniter import croniter, CroniterBadCronError
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now()
|
||||||
result = []
|
result = []
|
||||||
for s in schedules:
|
for s in schedules:
|
||||||
s = dict(s)
|
s = dict(s)
|
||||||
try:
|
try:
|
||||||
cron = croniter(s["cron_expr"], now)
|
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:
|
except CroniterBadCronError:
|
||||||
s["next_fire_at"] = "invalid expression"
|
s["next_fire_at"] = "invalid expression"
|
||||||
result.append(s)
|
result.append(s)
|
||||||
|
|||||||
@ -28,7 +28,7 @@
|
|||||||
{% for r in recent_runs %}
|
{% for r in recent_runs %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="/group-runs/{{ r.id }}">#{{ r.id }}</a></td>
|
<td><a href="/group-runs/{{ r.id }}">#{{ r.id }}</a></td>
|
||||||
<td class="mono">{{ r.started_at or "—" }}</td>
|
<td class="mono">{{ r.started_at | localtime }}</td>
|
||||||
<td class="mono">{{ r.duration_s | duration }}</td>
|
<td class="mono">{{ r.duration_s | duration }}</td>
|
||||||
<td class="mono">{{ r.triggered_by or "—" }}</td>
|
<td class="mono">{{ r.triggered_by or "—" }}</td>
|
||||||
<td><span class="pill {{ r.status }}">{{ r.status }}</span></td>
|
<td><span class="pill {{ r.status }}">{{ r.status }}</span></td>
|
||||||
|
|||||||
@ -31,7 +31,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><a href="/runs/{{ r.id }}">#{{ r.id }}</a></td>
|
<td><a href="/runs/{{ r.id }}">#{{ r.id }}</a></td>
|
||||||
<td><a href="/modules/{{ r.module_id }}">{{ r.module_name }}</a></td>
|
<td><a href="/modules/{{ r.module_id }}">{{ r.module_name }}</a></td>
|
||||||
<td class="mono">{{ r.started_at or "—" }}</td>
|
<td class="mono">{{ r.started_at | localtime }}</td>
|
||||||
<td class="mono">{{ r.duration_s | duration }}</td>
|
<td class="mono">{{ r.duration_s | duration }}</td>
|
||||||
<td><span class="pill {{ r.status }}">{{ r.status }}</span></td>
|
<td><span class="pill {{ r.status }}">{{ r.status }}</span></td>
|
||||||
<td class="mono">{{ r.row_count if r.row_count is not none else "—" }}</td>
|
<td class="mono">{{ r.row_count if r.row_count is not none else "—" }}</td>
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
{% for r in recent_runs %}
|
{% for r in recent_runs %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="/runs/{{ r.id }}">#{{ r.id }}</a></td>
|
<td><a href="/runs/{{ r.id }}">#{{ r.id }}</a></td>
|
||||||
<td class="mono">{{ r.started_at or '—' }}</td>
|
<td class="mono">{{ r.started_at | localtime }}</td>
|
||||||
<td class="mono">{{ r.duration_s | duration }}</td>
|
<td class="mono">{{ r.duration_s | duration }}</td>
|
||||||
<td><span class="pill {{ r.status }}">{{ r.status }}</span></td>
|
<td><span class="pill {{ r.status }}">{{ r.status }}</span></td>
|
||||||
<td class="mono">{{ r.row_count if r.row_count is not none else "—" }}</td>
|
<td class="mono">{{ r.row_count if r.row_count is not none else "—" }}</td>
|
||||||
|
|||||||
@ -67,7 +67,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>cron expression</th>
|
<th>cron expression</th>
|
||||||
<th>next fire (UTC)</th>
|
<th>next fire</th>
|
||||||
<th>last fired</th>
|
<th>last fired</th>
|
||||||
<th>enabled</th>
|
<th>enabled</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -77,7 +77,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="mono">{{ s.cron_expr }}</td>
|
<td class="mono">{{ s.cron_expr }}</td>
|
||||||
<td class="mono">{% if s.enabled %}{{ s.next_fire_at }}{% else %}—{% endif %}</td>
|
<td class="mono">{% if s.enabled %}{{ s.next_fire_at }}{% else %}—{% endif %}</td>
|
||||||
<td class="mono">{{ s.last_fired_at or "never" }}</td>
|
<td class="mono">{{ s.last_fired_at | localtime }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if s.enabled %}
|
{% if s.enabled %}
|
||||||
<span class="pill success">yes</span>
|
<span class="pill success">yes</span>
|
||||||
|
|||||||
@ -66,7 +66,7 @@
|
|||||||
<div class="panel" style="margin-top:1rem">
|
<div class="panel" style="margin-top:1rem">
|
||||||
<header>
|
<header>
|
||||||
Schedules
|
Schedules
|
||||||
<span class="subtitle">cron expressions in UTC — e.g. <code>0 4 * * *</code> for daily at 04:00</span>
|
<span class="subtitle">cron expressions in local time — e.g. <code>0 4 * * *</code> for daily at 04:00</span>
|
||||||
<button type="button" class="btn ghost" style="margin-left:auto"
|
<button type="button" class="btn ghost" style="margin-left:auto"
|
||||||
onclick="addScheduleRow()">+ add</button>
|
onclick="addScheduleRow()">+ add</button>
|
||||||
</header>
|
</header>
|
||||||
@ -106,6 +106,25 @@
|
|||||||
{% if not schedules %}
|
{% if not schedules %}
|
||||||
<div id="sched-empty" class="empty" style="margin-top:0.4rem">No schedules — group runs manually only.</div>
|
<div id="sched-empty" class="empty" style="margin-top:0.4rem">No schedules — group runs manually only.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<details style="margin-top:0.8rem">
|
||||||
|
<summary style="cursor:pointer;color:var(--text-muted);font-size:0.85em">cron expression reference</summary>
|
||||||
|
<table class="grid" style="margin-top:0.5rem;font-size:0.85em">
|
||||||
|
<thead><tr><th>expression</th><th>meaning</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td class="mono">0 * * * *</td><td>every hour (on the hour)</td></tr>
|
||||||
|
<tr><td class="mono">*/15 * * * *</td><td>every 15 minutes</td></tr>
|
||||||
|
<tr><td class="mono">0 4 * * *</td><td>daily at 04:00</td></tr>
|
||||||
|
<tr><td class="mono">0 4 * * 1-5</td><td>weekdays at 04:00</td></tr>
|
||||||
|
<tr><td class="mono">0 4 * * 6,0</td><td>weekends at 04:00</td></tr>
|
||||||
|
<tr><td class="mono">0 0 * * 1</td><td>every Monday at midnight</td></tr>
|
||||||
|
<tr><td class="mono">0 6,18 * * *</td><td>twice daily at 06:00 and 18:00</td></tr>
|
||||||
|
<tr><td class="mono">0 0 1 * *</td><td>first day of each month</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div style="margin-top:0.4rem;color:var(--text-muted);font-size:0.82em">
|
||||||
|
Format: <code>minute hour day-of-month month day-of-week</code> · all times local
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
Group run #{{ group_run.id }}
|
Group run #{{ group_run.id }}
|
||||||
<span class="subtitle">
|
<span class="subtitle">
|
||||||
<a href="/groups/{{ group_run.group_id }}">{{ group_run.group_name }}</a> ·
|
<a href="/groups/{{ group_run.group_id }}">{{ group_run.group_name }}</a> ·
|
||||||
started {{ group_run.started_at }}
|
started {{ group_run.started_at | localtime }}
|
||||||
</span>
|
</span>
|
||||||
<span style="margin-left:auto">
|
<span style="margin-left:auto">
|
||||||
<span class="pill {{ group_run.status }}">{{ group_run.status }}</span>
|
<span class="pill {{ group_run.status }}">{{ group_run.status }}</span>
|
||||||
@ -16,8 +16,8 @@
|
|||||||
</header>
|
</header>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<dl class="keyval">
|
<dl class="keyval">
|
||||||
<dt>started</dt> <dd class="mono">{{ group_run.started_at }}</dd>
|
<dt>started</dt> <dd class="mono">{{ group_run.started_at | localtime }}</dd>
|
||||||
<dt>finished</dt> <dd class="mono">{{ group_run.finished_at or "—" }}</dd>
|
<dt>finished</dt> <dd class="mono">{{ group_run.finished_at | localtime }}</dd>
|
||||||
<dt>duration</dt> <dd class="mono">{{ group_run.duration_s | duration }}</dd>
|
<dt>duration</dt> <dd class="mono">{{ group_run.duration_s | duration }}</dd>
|
||||||
<dt>triggered by</dt><dd class="mono">{{ group_run.triggered_by or "—" }}</dd>
|
<dt>triggered by</dt><dd class="mono">{{ group_run.triggered_by or "—" }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
@ -50,7 +50,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><a href="/runs/{{ r.id }}">#{{ r.id }}</a></td>
|
<td><a href="/runs/{{ r.id }}">#{{ r.id }}</a></td>
|
||||||
<td><a href="/modules/{{ r.module_id }}">{{ r.module_name }}</a></td>
|
<td><a href="/modules/{{ r.module_id }}">{{ r.module_name }}</a></td>
|
||||||
<td class="mono">{{ r.started_at or "—" }}</td>
|
<td class="mono">{{ r.started_at | localtime }}</td>
|
||||||
<td class="mono">{{ r.duration_s | duration }}</td>
|
<td class="mono">{{ r.duration_s | duration }}</td>
|
||||||
<td><span class="pill {{ r.status }}">{{ r.status }}</span></td>
|
<td><span class="pill {{ r.status }}">{{ r.status }}</span></td>
|
||||||
<td class="mono">{{ r.row_count if r.row_count is not none else "—" }}</td>
|
<td class="mono">{{ r.row_count if r.row_count is not none else "—" }}</td>
|
||||||
|
|||||||
@ -28,7 +28,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><a href="/groups/{{ g.id }}"><strong>{{ g.name }}</strong></a></td>
|
<td><a href="/groups/{{ g.id }}"><strong>{{ g.name }}</strong></a></td>
|
||||||
<td class="mono">{{ g.member_count }}</td>
|
<td class="mono">{{ g.member_count }}</td>
|
||||||
<td class="mono">{{ g.last_run_at or "—" }}</td>
|
<td class="mono">{{ g.last_run_at | localtime }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if g.last_status %}
|
{% if g.last_status %}
|
||||||
<span class="pill {{ g.last_status }}">{{ g.last_status }}</span>
|
<span class="pill {{ g.last_status }}">{{ g.last_status }}</span>
|
||||||
|
|||||||
@ -39,7 +39,7 @@
|
|||||||
<a href="/groups/{{ g.group_id }}" class="tag">{{ g.group_name }}</a>
|
<a href="/groups/{{ g.group_id }}" class="tag">{{ g.group_name }}</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td class="mono">{{ m.last_run_at or "—" }}</td>
|
<td class="mono">{{ m.last_run_at | localtime }}</td>
|
||||||
{% with module=m %}{% include "_module_status_pill.html" %}{% endwith %}
|
{% with module=m %}{% include "_module_status_pill.html" %}{% endwith %}
|
||||||
<td class="mono">{{ m.last_row_count if m.last_row_count is not none else "—" }}</td>
|
<td class="mono">{{ m.last_row_count if m.last_row_count is not none else "—" }}</td>
|
||||||
<td style="text-align:right">
|
<td style="text-align:right">
|
||||||
|
|||||||
@ -8,14 +8,14 @@
|
|||||||
Run #{{ run.id }}
|
Run #{{ run.id }}
|
||||||
<span class="subtitle">
|
<span class="subtitle">
|
||||||
<a href="/modules/{{ run.module_id }}">{{ run.module_name }}</a> ·
|
<a href="/modules/{{ run.module_id }}">{{ run.module_name }}</a> ·
|
||||||
started {{ run.started_at }}
|
started {{ run.started_at | localtime }}
|
||||||
</span>
|
</span>
|
||||||
<span style="margin-left:auto"><span class="pill {{ run.status }}">{{ run.status }}</span></span>
|
<span style="margin-left:auto"><span class="pill {{ run.status }}">{{ run.status }}</span></span>
|
||||||
</header>
|
</header>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<dl class="keyval">
|
<dl class="keyval">
|
||||||
<dt>started</dt> <dd class="mono">{{ run.started_at }}</dd>
|
<dt>started</dt> <dd class="mono">{{ run.started_at | localtime }}</dd>
|
||||||
<dt>finished</dt> <dd class="mono">{{ run.finished_at or '—' }}</dd>
|
<dt>finished</dt> <dd class="mono">{{ run.finished_at | localtime }}</dd>
|
||||||
<dt>rows</dt> <dd class="mono">{{ run.row_count if run.row_count is not none else '—' }}</dd>
|
<dt>rows</dt> <dd class="mono">{{ run.row_count if run.row_count is not none else '—' }}</dd>
|
||||||
<dt>watermarks</dt><dd class="mono">{{ run.watermark_values_json or '—' }}</dd>
|
<dt>watermarks</dt><dd class="mono">{{ run.watermark_values_json or '—' }}</dd>
|
||||||
{% if run.error %}<dt>error</dt><dd class="mono" style="color:var(--danger)">{{ run.error }}</dd>{% endif %}
|
{% if run.error %}<dt>error</dt><dd class="mono" style="color:var(--danger)">{{ run.error }}</dd>{% endif %}
|
||||||
|
|||||||
@ -33,7 +33,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><a href="/runs/{{ r.id }}">#{{ r.id }}</a></td>
|
<td><a href="/runs/{{ r.id }}">#{{ r.id }}</a></td>
|
||||||
<td><a href="/modules/{{ r.module_id }}">{{ r.module_name }}</a></td>
|
<td><a href="/modules/{{ r.module_id }}">{{ r.module_name }}</a></td>
|
||||||
<td class="mono">{{ r.started_at or '—' }}</td>
|
<td class="mono">{{ r.started_at | localtime }}</td>
|
||||||
<td class="mono">{{ r.duration_s | duration }}</td>
|
<td class="mono">{{ r.duration_s | duration }}</td>
|
||||||
<td><span class="pill {{ r.status }}">{{ r.status }}</span></td>
|
<td><span class="pill {{ r.status }}">{{ r.status }}</span></td>
|
||||||
<td class="mono">{{ r.row_count if r.row_count is not none else "—" }}</td>
|
<td class="mono">{{ r.row_count if r.row_count is not none else "—" }}</td>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user