tweak job runs
This commit is contained in:
parent
2b2a3f1cc0
commit
c210168d65
6 changed files with 94 additions and 12 deletions
|
|
@ -112,5 +112,6 @@ uv run repub crawl -c repub.toml
|
|||
- Runtime ffmpeg availability is provided by the flake package and devshell.
|
||||
- Tests live under `tests/`.
|
||||
- `prompts/` is git ignored intentionally
|
||||
- Never search the web for this repo. If an external resource, document, or reference is needed, stop and ask the user to provide it.
|
||||
- Treat the repo-root `republisher.db` as user-owned local state. Do not delete or reset it as part of routine testing or verification.
|
||||
- For automated tests or isolated verification, use a separate database path via `REPUBLISHER_DB_PATH` instead of mutating or removing the repo-root database.
|
||||
|
|
|
|||
|
|
@ -403,7 +403,7 @@ def status_badge(*, label: str, tone: str) -> Renderable:
|
|||
"scheduled": "bg-sky-100 text-sky-800",
|
||||
"idle": "bg-slate-200 text-slate-700",
|
||||
"failed": "bg-rose-100 text-rose-800",
|
||||
"done": "bg-amber-100 text-amber-800",
|
||||
"done": "bg-emerald-100 text-emerald-800",
|
||||
}
|
||||
return h.span(
|
||||
class_=f"inline-flex rounded-full px-2.5 py-1 text-xs font-semibold {tones[tone]}"
|
||||
|
|
|
|||
|
|
@ -504,10 +504,11 @@ def _project_upcoming_job(
|
|||
"slug": job.source.slug,
|
||||
"job_id": job_id,
|
||||
"next_run": (
|
||||
next_run.strftime("%Y-%m-%d %H:%M UTC")
|
||||
_humanize_future_time(reference_time, next_run)
|
||||
if next_run is not None
|
||||
else ("Running now" if running_execution is not None else "Not scheduled")
|
||||
),
|
||||
"next_run_at": next_run.isoformat() if next_run is not None else None,
|
||||
"schedule": " ".join(
|
||||
(
|
||||
str(job.cron_minute),
|
||||
|
|
@ -641,3 +642,22 @@ def _format_bytes(value: int) -> str:
|
|||
if value < 1024 * 1024 * 1024:
|
||||
return f"{value / (1024 * 1024):.1f} MB"
|
||||
return f"{value / (1024 * 1024 * 1024):.1f} GB"
|
||||
|
||||
|
||||
def _humanize_future_time(reference_time: datetime, target_time: datetime) -> str:
|
||||
delta_seconds = int(round((target_time - reference_time).total_seconds()))
|
||||
if delta_seconds <= 0:
|
||||
return "now"
|
||||
|
||||
units = (
|
||||
("day", 24 * 60 * 60),
|
||||
("hour", 60 * 60),
|
||||
("minute", 60),
|
||||
)
|
||||
for label, size in units:
|
||||
if delta_seconds >= size:
|
||||
count = max(1, round(delta_seconds / size))
|
||||
suffix = "" if count == 1 else "s"
|
||||
return f"in {count} {label}{suffix}"
|
||||
|
||||
return f"in {delta_seconds} seconds"
|
||||
|
|
|
|||
|
|
@ -70,9 +70,6 @@ def _running_row(execution: Mapping[str, object]) -> tuple[Node, ...]:
|
|||
h.p(class_="font-medium text-slate-900")[
|
||||
f"#{_text(execution, 'execution_id')}"
|
||||
],
|
||||
h.p(class_="mt-1 text-xs text-slate-500")[
|
||||
f"job {_text(execution, 'job_id')}"
|
||||
],
|
||||
],
|
||||
h.div[
|
||||
h.p(class_="font-medium text-slate-900")[_text(execution, "started_at")],
|
||||
|
|
@ -99,15 +96,26 @@ def _running_row(execution: Mapping[str, object]) -> tuple[Node, ...]:
|
|||
|
||||
|
||||
def _upcoming_row(job: Mapping[str, object]) -> tuple[Node, ...]:
|
||||
next_run_at = _maybe_text(job, "next_run_at")
|
||||
next_run_label: Node = h.p(class_="font-medium text-slate-900")[
|
||||
_text(job, "next_run")
|
||||
]
|
||||
if next_run_at is not None:
|
||||
next_run_label = h.time(
|
||||
{
|
||||
"data-next-run-at": next_run_at,
|
||||
"title": next_run_at,
|
||||
},
|
||||
datetime=next_run_at,
|
||||
class_="font-medium text-slate-900",
|
||||
)[_text(job, "next_run")]
|
||||
|
||||
return (
|
||||
h.div[
|
||||
h.div(class_="font-semibold text-slate-950")[_text(job, "source")],
|
||||
h.p(class_="mt-1 font-mono text-xs text-slate-500")[_text(job, "slug")],
|
||||
],
|
||||
h.div[
|
||||
h.p(class_="font-medium text-slate-900")[_text(job, "next_run")],
|
||||
h.p(class_="mt-1 text-xs text-slate-500")[f"job {_text(job, 'job_id')}"],
|
||||
],
|
||||
h.div[next_run_label,],
|
||||
h.p(class_="font-mono text-xs text-slate-600")[_text(job, "schedule")],
|
||||
status_badge(
|
||||
label=_text(job, "enabled_label"),
|
||||
|
|
@ -147,9 +155,6 @@ def _completed_row(execution: Mapping[str, object]) -> tuple[Node, ...]:
|
|||
h.p(class_="font-medium text-slate-900")[
|
||||
f"#{_text(execution, 'execution_id')}"
|
||||
],
|
||||
h.p(class_="mt-1 text-xs text-slate-500")[
|
||||
f"job {_text(execution, 'job_id')}"
|
||||
],
|
||||
],
|
||||
h.div[
|
||||
h.p(class_="font-medium text-slate-900")[_text(execution, "ended_at")],
|
||||
|
|
@ -232,6 +237,48 @@ def runs_page(
|
|||
),
|
||||
rows=completed_rows,
|
||||
),
|
||||
h.script[
|
||||
"""
|
||||
window.repubFormatNextRuns = window.repubFormatNextRuns || (() => {
|
||||
const relativeFormatter = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' });
|
||||
const absoluteFormatter = new Intl.DateTimeFormat(undefined, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
timeZoneName: 'short',
|
||||
});
|
||||
const formatRelative = (targetDate) => {
|
||||
const diffSeconds = Math.round((targetDate.getTime() - Date.now()) / 1000);
|
||||
const units = [
|
||||
['day', 86400],
|
||||
['hour', 3600],
|
||||
['minute', 60],
|
||||
['second', 1],
|
||||
];
|
||||
for (const [unit, size] of units) {
|
||||
if (Math.abs(diffSeconds) >= size || unit === 'second') {
|
||||
return relativeFormatter.format(Math.round(diffSeconds / size), unit);
|
||||
}
|
||||
}
|
||||
return relativeFormatter.format(0, 'second');
|
||||
};
|
||||
const format = () => {
|
||||
document.querySelectorAll('time[data-next-run-at]').forEach((element) => {
|
||||
const nextRunAt = element.getAttribute('data-next-run-at');
|
||||
if (!nextRunAt) return;
|
||||
const targetDate = new Date(nextRunAt);
|
||||
if (Number.isNaN(targetDate.getTime())) return;
|
||||
element.textContent = formatRelative(targetDate);
|
||||
element.title = absoluteFormatter.format(targetDate);
|
||||
});
|
||||
};
|
||||
format();
|
||||
if (!window.repubNextRunTimer) {
|
||||
window.repubNextRunTimer = window.setInterval(format, 30000);
|
||||
}
|
||||
});
|
||||
window.repubFormatNextRuns();
|
||||
"""
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
/*! tailwindcss v4.2.1 | MIT License | https://tailwindcss.com */
|
||||
@view-transition {
|
||||
navigation: auto;
|
||||
}
|
||||
|
||||
@layer properties;
|
||||
@layer theme, base, components, utilities;
|
||||
@layer theme {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import asyncio
|
|||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
|
||||
from repub.components import status_badge
|
||||
from repub.datastar import RefreshBroker, render_sse_event, render_stream
|
||||
from repub.model import (
|
||||
Job,
|
||||
|
|
@ -26,6 +27,13 @@ from repub.web import (
|
|||
)
|
||||
|
||||
|
||||
def test_status_badge_uses_green_done_tone() -> None:
|
||||
badge = str(status_badge(label="Succeeded", tone="done"))
|
||||
|
||||
assert "bg-emerald-100 text-emerald-800" in badge
|
||||
assert "Succeeded" in badge
|
||||
|
||||
|
||||
def test_root_get_serves_datastar_shim() -> None:
|
||||
async def run() -> None:
|
||||
client = create_app().test_client()
|
||||
|
|
@ -618,6 +626,8 @@ def test_render_runs_shows_running_upcoming_and_completed_tables(
|
|||
assert "Completed job executions" in body
|
||||
assert "runs-render-source" in body
|
||||
assert f"/job/{job.id}/execution/{execution.get_id()}/logs" in body
|
||||
assert "data-next-run-at" in body
|
||||
assert "in " in body
|
||||
assert "Already running" not in body
|
||||
|
||||
asyncio.run(run())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue