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.
|
- Runtime ffmpeg availability is provided by the flake package and devshell.
|
||||||
- Tests live under `tests/`.
|
- Tests live under `tests/`.
|
||||||
- `prompts/` is git ignored intentionally
|
- `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.
|
- 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.
|
- 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",
|
"scheduled": "bg-sky-100 text-sky-800",
|
||||||
"idle": "bg-slate-200 text-slate-700",
|
"idle": "bg-slate-200 text-slate-700",
|
||||||
"failed": "bg-rose-100 text-rose-800",
|
"failed": "bg-rose-100 text-rose-800",
|
||||||
"done": "bg-amber-100 text-amber-800",
|
"done": "bg-emerald-100 text-emerald-800",
|
||||||
}
|
}
|
||||||
return h.span(
|
return h.span(
|
||||||
class_=f"inline-flex rounded-full px-2.5 py-1 text-xs font-semibold {tones[tone]}"
|
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,
|
"slug": job.source.slug,
|
||||||
"job_id": job_id,
|
"job_id": job_id,
|
||||||
"next_run": (
|
"next_run": (
|
||||||
next_run.strftime("%Y-%m-%d %H:%M UTC")
|
_humanize_future_time(reference_time, next_run)
|
||||||
if next_run is not None
|
if next_run is not None
|
||||||
else ("Running now" if running_execution is not None else "Not scheduled")
|
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(
|
"schedule": " ".join(
|
||||||
(
|
(
|
||||||
str(job.cron_minute),
|
str(job.cron_minute),
|
||||||
|
|
@ -641,3 +642,22 @@ def _format_bytes(value: int) -> str:
|
||||||
if value < 1024 * 1024 * 1024:
|
if value < 1024 * 1024 * 1024:
|
||||||
return f"{value / (1024 * 1024):.1f} MB"
|
return f"{value / (1024 * 1024):.1f} MB"
|
||||||
return f"{value / (1024 * 1024 * 1024):.1f} GB"
|
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")[
|
h.p(class_="font-medium text-slate-900")[
|
||||||
f"#{_text(execution, 'execution_id')}"
|
f"#{_text(execution, 'execution_id')}"
|
||||||
],
|
],
|
||||||
h.p(class_="mt-1 text-xs text-slate-500")[
|
|
||||||
f"job {_text(execution, 'job_id')}"
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
h.div[
|
h.div[
|
||||||
h.p(class_="font-medium text-slate-900")[_text(execution, "started_at")],
|
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, ...]:
|
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 (
|
return (
|
||||||
h.div[
|
h.div[
|
||||||
h.div(class_="font-semibold text-slate-950")[_text(job, "source")],
|
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.p(class_="mt-1 font-mono text-xs text-slate-500")[_text(job, "slug")],
|
||||||
],
|
],
|
||||||
h.div[
|
h.div[next_run_label,],
|
||||||
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.p(class_="font-mono text-xs text-slate-600")[_text(job, "schedule")],
|
h.p(class_="font-mono text-xs text-slate-600")[_text(job, "schedule")],
|
||||||
status_badge(
|
status_badge(
|
||||||
label=_text(job, "enabled_label"),
|
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")[
|
h.p(class_="font-medium text-slate-900")[
|
||||||
f"#{_text(execution, 'execution_id')}"
|
f"#{_text(execution, 'execution_id')}"
|
||||||
],
|
],
|
||||||
h.p(class_="mt-1 text-xs text-slate-500")[
|
|
||||||
f"job {_text(execution, 'job_id')}"
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
h.div[
|
h.div[
|
||||||
h.p(class_="font-medium text-slate-900")[_text(execution, "ended_at")],
|
h.p(class_="font-medium text-slate-900")[_text(execution, "ended_at")],
|
||||||
|
|
@ -232,6 +237,48 @@ def runs_page(
|
||||||
),
|
),
|
||||||
rows=completed_rows,
|
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 */
|
/*! tailwindcss v4.2.1 | MIT License | https://tailwindcss.com */
|
||||||
|
@view-transition {
|
||||||
|
navigation: auto;
|
||||||
|
}
|
||||||
|
|
||||||
@layer properties;
|
@layer properties;
|
||||||
@layer theme, base, components, utilities;
|
@layer theme, base, components, utilities;
|
||||||
@layer theme {
|
@layer theme {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import asyncio
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
|
from repub.components import status_badge
|
||||||
from repub.datastar import RefreshBroker, render_sse_event, render_stream
|
from repub.datastar import RefreshBroker, render_sse_event, render_stream
|
||||||
from repub.model import (
|
from repub.model import (
|
||||||
Job,
|
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:
|
def test_root_get_serves_datastar_shim() -> None:
|
||||||
async def run() -> None:
|
async def run() -> None:
|
||||||
client = create_app().test_client()
|
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 "Completed job executions" in body
|
||||||
assert "runs-render-source" in body
|
assert "runs-render-source" in body
|
||||||
assert f"/job/{job.id}/execution/{execution.get_id()}/logs" 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
|
assert "Already running" not in body
|
||||||
|
|
||||||
asyncio.run(run())
|
asyncio.run(run())
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue