Job::start() calls setLastRun() before run() and clears `reserved_at` only
afterwards through setExecutionTime(), which is not in a finally block. Any
uncaught throwable therefore leaves the job reserved, and JobList::getNext()
skips reserved jobs until the reservation is 12 hours stale -- so a single
failing workflow takes the whole app offline for half a day, while last_run
still shows the start of that failed attempt.
Wrap both loops so a failure is logged with its exception and the remaining
users and workflows still run.
Ports the standalone due-date cron script into a proper Nextcloud 34
app with a personal-settings UI, per-user workflow configuration
(source/target stack, assigned-user and label filters, email
notification), a TimedJob background runner, and Gitea CI pipelines.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>