Improve daily jobs search metadata and modal link

This commit is contained in:
Ivo Oskamp 2026-02-16 16:26:56 +01:00
parent dc3eb2f73c
commit ded71cb50f
3 changed files with 165 additions and 4 deletions

View File

@ -1,5 +1,12 @@
from .routes_shared import * # noqa: F401,F403
from .routes_shared import _format_datetime
from .routes_shared import (
_apply_overrides_to_run,
_format_datetime,
_get_or_create_settings,
_get_ui_timezone,
_infer_monthly_schedule_from_runs,
_infer_schedule_map_from_runs,
)
from sqlalchemy import and_, cast, func, or_, String
import math
@ -276,6 +283,65 @@ def _build_daily_jobs_results(patterns: list[str], page: int) -> dict:
if not _is_section_allowed("daily_jobs"):
return section
try:
tz = _get_ui_timezone()
except Exception:
tz = None
try:
target_date = datetime.now(tz).date() if tz else datetime.utcnow().date()
except Exception:
target_date = datetime.utcnow().date()
settings = _get_or_create_settings()
missed_start_date = getattr(settings, "daily_jobs_start_date", None)
if tz:
local_midnight = datetime(
year=target_date.year,
month=target_date.month,
day=target_date.day,
hour=0,
minute=0,
second=0,
tzinfo=tz,
)
start_of_day = local_midnight.astimezone(datetime_module.timezone.utc).replace(tzinfo=None)
end_of_day = (local_midnight + timedelta(days=1)).astimezone(datetime_module.timezone.utc).replace(tzinfo=None)
else:
start_of_day = datetime(
year=target_date.year,
month=target_date.month,
day=target_date.day,
hour=0,
minute=0,
second=0,
)
end_of_day = start_of_day + timedelta(days=1)
def _to_local(dt_utc):
if not dt_utc or not tz:
return dt_utc
try:
if dt_utc.tzinfo is None:
dt_utc = dt_utc.replace(tzinfo=datetime_module.timezone.utc)
return dt_utc.astimezone(tz)
except Exception:
return dt_utc
def _bucket_15min(dt_utc):
d = _to_local(dt_utc)
if not d:
return None
minute_bucket = (d.minute // 15) * 15
return f"{d.hour:02d}:{minute_bucket:02d}"
def _is_success_status(value: str) -> bool:
s = (value or "").strip().lower()
if not s:
return False
return ("success" in s) or ("override" in s)
query = (
db.session.query(
Job.id.label("job_id"),
@ -314,12 +380,71 @@ def _build_daily_jobs_results(patterns: list[str], page: int) -> dict:
)
_enrich_paging(section, total, current_page, total_pages)
for row in rows:
expected_times = (_infer_schedule_map_from_runs(row.job_id).get(target_date.weekday()) or [])
if not expected_times:
monthly = _infer_monthly_schedule_from_runs(row.job_id)
if monthly:
try:
dom = int(monthly.get("day_of_month") or 0)
except Exception:
dom = 0
mtimes = monthly.get("times") or []
try:
import calendar as _calendar
last_dom = _calendar.monthrange(target_date.year, target_date.month)[1]
except Exception:
last_dom = target_date.day
scheduled_dom = dom if (dom and dom <= last_dom) else last_dom
if target_date.day == scheduled_dom:
expected_times = list(mtimes)
runs_for_day = (
JobRun.query.filter(
JobRun.job_id == row.job_id,
JobRun.run_at >= start_of_day,
JobRun.run_at < end_of_day,
)
.order_by(JobRun.run_at.asc())
.all()
)
run_count = len(runs_for_day)
last_status = "-"
expected_display = expected_times[-1] if expected_times else "-"
if run_count > 0:
last_run = runs_for_day[-1]
try:
job_obj = Job.query.get(int(row.job_id))
status_display, _override_applied, _override_level, _ov_id, _ov_reason = _apply_overrides_to_run(job_obj, last_run)
if getattr(last_run, "missed", False):
last_status = status_display or "Missed"
else:
last_status = status_display or (last_run.status or "-")
except Exception:
last_status = last_run.status or "-"
expected_display = _bucket_15min(last_run.run_at) or expected_display
else:
try:
today_local = datetime.now(tz).date() if tz else datetime.utcnow().date()
except Exception:
today_local = datetime.utcnow().date()
if target_date > today_local:
last_status = "Expected"
elif target_date == today_local:
last_status = "Expected"
else:
if missed_start_date and target_date < missed_start_date:
last_status = "-"
else:
last_status = "Missed"
success_text = "Yes" if _is_success_status(last_status) else "No"
section["items"].append(
{
"title": row.job_name or f"Job #{row.job_id}",
"subtitle": f"{row.customer_name or '-'} | {row.backup_software or '-'} / {row.backup_type or '-'}",
"meta": "",
"link": url_for("main.daily_jobs"),
"meta": f"Expected: {expected_display} | Successful: {success_text} | Runs: {run_count}",
"link": url_for("main.daily_jobs", date=target_date.strftime("%Y-%m-%d"), open_job_id=row.job_id),
}
)

View File

@ -771,9 +771,43 @@ if (tStatus) tStatus.textContent = '';
});
}
function autoOpenJobFromQuery() {
try {
var params = new URLSearchParams(window.location.search || "");
var openJobId = (params.get("open_job_id") || "").trim();
if (!openJobId) {
return;
}
var rows = document.querySelectorAll(".daily-job-row");
var targetRow = null;
rows.forEach(function (row) {
if ((row.getAttribute("data-job-id") || "") === openJobId) {
targetRow = row;
}
});
if (!targetRow) {
return;
}
targetRow.click();
params.delete("open_job_id");
var nextQuery = params.toString();
var nextUrl = window.location.pathname + (nextQuery ? ("?" + nextQuery) : "");
if (window.history && window.history.replaceState) {
window.history.replaceState({}, document.title, nextUrl);
}
} catch (e) {
// no-op
}
}
document.addEventListener("DOMContentLoaded", function () {
bindInlineCreateForms();
attachDailyJobsHandlers();
autoOpenJobFromQuery();
});
})();
</script>

View File

@ -18,6 +18,8 @@ This file documents all changes made to this project via Claude Code.
- Changed global search visibility to only include sections accessible to the currently active role
- Changed `docs/technical-notes-codex.md` with a dedicated Global Grouped Search section (route/UI/behavior/access rules) and latest test build digest for `v20260216-02-global-search`
- Changed global search to support per-section pagination (previous/next) so results beyond the first 10 can be browsed per section while preserving the current query/state
- Changed Daily Jobs search result metadata to include expected run time, success indicator, and run count for the selected day
- Changed Daily Jobs search result links to open the same Daily Jobs modal flow via `open_job_id` (instead of only navigating to the overview page)
### Fixed
- Fixed `/search` page crash (`TypeError: 'builtin_function_or_method' object is not iterable`) by replacing Jinja dict access from `section.items` to `section['items']` in `templates/main/search.html`