Skip to content

Keep repeating tasks visible after each run - #15093

Merged
rtibbles merged 7 commits into
learningequality:developfrom
rtibblesbot:issue-15092-bb7f7f
Jul 31, 2026
Merged

Keep repeating tasks visible after each run#15093
rtibbles merged 7 commits into
learningequality:developfrom
rtibblesbot:issue-15092-bb7f7f

Conversation

@rtibblesbot

@rtibblesbot rtibblesbot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

A re-queued repeating job's own state no longer describes the run that just ended.

  • Record each run's final state and time on the job row, and expose them on the tasks API.
  • Derive the displayed status from that snapshot, so the task stays listed as finished-and-scheduled-again, and counts as done in the Device > Facilities row and the tasks bar.
  • Reset per-run fields on start, so a second run doesn't inherit the previous run's progress bar and exception.
  • End the Facility > Data spinner on the snapshot changing, and stop clearing a repeating row — clearing it would delete the schedule.
  • Add a "Last sync" column to Manage sync schedule, read from the same snapshot.

Incidental, hit while running the auth suite: multiple_kolibri_servers now shuts down the servers it already started when a later one fails to start. They were left holding connections to their test databases, so one failed start turned into a failure in every test after it.

References

Fixes #15092. First seen: #15052 (comment).

Reviewer guidance

  • Confirm that completed KDP syncs do not get automatically cleared.
  • Confirm that information about subsequent syncs displays properly
  • Confirm that manage sync schedule still works as intended and displays the last sync

Screenshots

Walked every acceptance criterion on two live Kolibri servers — device A (:8451) owning "QA Facility", device B (:8452) having imported it — with a real peer sync schedule between them.

State Screenshot
Finished, on the schedule — Finished 30 seconds ago, next sync in 1 hour, 10 KB sent • 23 KB received, no Clear/Retry/Cancel Finished
Failed run — Failed 20 seconds ago, retrying in 5 minutes Failed
Never-run schedule is the only task — "no tasks" message Empty
Peer without a schedule — one-off repeat: 0 entry, still clearable One-off
Manage sync schedule — new "Last sync" column Schedule
Facilities row During the run After the run
"Syncing" only while status === RUNNING; tasks bar arithmetic Syncing Done

Also confirmed by sampling the API through each run:

  • Reload — the entry is unchanged; the snapshot is server-side, no client state.
  • "Clear completed"POST /api/tasks/tasks/clearall/ returns 200 and leaves both the row and the schedule in place.
  • Second run starts clean — at the transition into RUNNING the row reports exception: null and percentage: 0; the previous run's CommandError and traceback are gone, and the panel reads "Waiting" rather than "Finished" even though extra_metadata.sync_state still holds the previous run's COMPLETED.
  • Facility > Data — the spinner stops on the run boundary (last_finished_datetime changes), and repeat_interval/retry_interval survive, so TaskResource.clear is correctly not called on a schedule.

axe-core audit on the task manager and Manage sync schedule: 0 violations at WCAG AA.

AI usage

Used Claude Code to implement the change from a pre-approved plan, writing the failing tests before each piece. Verified with the tasks and auth Python suites, the kolibri-common/device/facility Jest suites, prek, and manual QA in a browser against a seeded device.


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?
  • Ran pre-flight CI checks (lint, format, tests) and verified all pass
  • Rebased onto the target branch and resolved any conflicts
  • Reorganized commit history into clean, logical commits
  • Audited the diff to ensure only issue-relevant files are changed
  • Built PR body from the repository's PR template with evidence blocks

Deviations from the issue spec

  • Both writes live in _write_job_update, not in reschedule_finished_job_if_needed and mark_job_as_running as the spec named them. One write point covers all three reschedule exits and both mark-as-running paths without duplicating the condition.
  • Consequently the snapshot is written on every finished transition, including one-off jobs, rather than only when a job is re-scheduled — so the spec's "no snapshot when a finished job is not re-scheduled" test is not written. A one-off row's snapshot is inert: showsLastRun requires a non-terminal status before it reads the snapshot.
  • Manual QA used a peer sync schedule rather than the Data Portal: a KDP sync from a dev device pushes real data to the live data portal, so the peer path exercises the same repeating-job code without that.
@rtibblesbot

🟡 Waiting for feedback

Last updated: 2026-07-31 14:29 UTC

@github-actions github-actions Bot added DEV: backend Python, databases, networking, filesystem... APP: Device Re: Device App (content import/export, facility-syncing, user permissions, etc.) APP: Facility Re: Facility App (user/class management, facility settings, csv import/export, etc.) DEV: frontend SIZE: large labels Jul 27, 2026
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

@rtibblesbot
rtibblesbot force-pushed the issue-15092-bb7f7f branch 3 times, most recently from 405925b to 6d8f79e Compare July 27, 2026 20:53
@rtibblesbot
rtibblesbot marked this pull request as ready for review July 27, 2026 21:35

@rtibbles rtibbles left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally looking OK, some thoughts about cleanup, and there may be other redundancies I've not spotted in a first look.

If there is other unnecessary indirection in the code, that should be cleaned up too.

Also, 9 commits for this seems too much - once you've made the relevant changes compress it to a logical series of commits.

Comment thread packages/kolibri-common/uiText/tasks.js Outdated
'Status of a task run that was canceled, with a relative time such as "5 minutes ago".',
},
taskStatusWithNextRun: {
message: '{status} · {nextRun}',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be better constructed as an RTL sensitive text join rather than a string.

@rtibblesbot rtibblesbot Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — taskStatusWithNextRun is gone, and syncFacilityTaskDisplayInfo now joins the two clauses with formatList from kolibri/utils/i18n (the Intl.ListFormat wrapper), so the separator and the ordering come from the locale rather than a hard-coded string.

One consequence worth flagging: it drops the middot the issue described, because Intl.ListFormat uses the locale's own list separator (a, b in en, a وb in ar). Say the word if you would rather keep the middot and I will build the join a different way.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still in place after the rebase onto develop — no code change this cycle. The join is formatList([getLastRunStatusMsg(task), nextRunMsg(task)]) at packages/kolibri-common/utils/syncTaskUtils.js:208; tasks.js only holds the two independently-translatable clauses.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No change this cycle — the join is still formatList([getLastRunStatusMsg(task), nextRunMsg(task)]) (syncTaskUtils.js:208), with the two clauses translated independently in tasks.js.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unchanged — the join is still formatList([getLastRunStatusMsg(task), nextRunMsg(task)]) (syncTaskUtils.js:214), with the two clauses translated separately in tasks.js.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No code change this cycle — the only work was diagnosing the Morango/PostgreSQL CI failure. The RTL-sensitive join is unchanged: formatList([getLastRunStatusMsg(task), nextRunMsg(task)]) at packages/kolibri-common/utils/syncTaskUtils.js:214, with the two clauses translated independently in tasks.js.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No change this cycle — this cycle was CI-only (diagnosing and fixing the Morango/PostgreSQL job). The RTL-sensitive join is still formatList([getLastRunStatusMsg(task), nextRunMsg(task)]) at packages/kolibri-common/utils/syncTaskUtils.js:214, with the two clauses translated independently in tasks.js.

retryInterval = null,
}) {
const now = Date.now();
return {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this very similar to the data in the syncSchedule.js test helper you already created? You did that based on two usages, so I'm confused why a third wasn't integrated.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair — that was my miss. The helper has moved to packages/kolibri-common/utils/__tests__/syncSchedule.js (next to the module it describes) and all three specs now build their rows from it. It grew hoursUntilNext, syncState and retryInterval knobs to cover this file's cases, and makeRescheduledTask is gone.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still in place after the rebase — no code change this cycle. The fixture is packages/kolibri-common/utils/__tests__/syncSchedule.js and all three specs build their rows from it (syncTaskUtils.spec.js:13, FacilitiesTasksPage.spec.js:5, FacilitiesPage.spec.js:5); no local task-literal builders remain.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No change this cycle — all three specs still build their rows from packages/kolibri-common/utils/__tests__/syncSchedule.js (syncTaskUtils.spec.js:13, FacilitiesTasksPage.spec.js:5, FacilitiesPage.spec.js:11).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unchanged — all three specs still build their rows from packages/kolibri-common/utils/__tests__/syncSchedule.js (syncTaskUtils.spec.js:13, FacilitiesTasksPage.spec.js:5, FacilitiesPage.spec.js:11).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No code change this cycle (CI diagnosis only). All three specs still build their rows from the shared packages/kolibri-common/utils/__tests__/syncSchedule.js fixture — syncTaskUtils.spec.js:13, FacilitiesTasksPage.spec.js:5, FacilitiesPage.spec.js:11.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No change this cycle (CI-only work). All three specs still build their rows from the shared packages/kolibri-common/utils/__tests__/syncSchedule.js fixture — syncTaskUtils.spec.js:13, FacilitiesTasksPage.spec.js:5, FacilitiesPage.spec.js:11.

}

const relativeStatusToDescriptionMap = {
[TaskStatuses.COMPLETED]: relativeTime =>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't these just be a mapping from a task type to a string id? They all use the same relative time argument, so repeating the function is pointless.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — it is now a status-to-string-id map and getLastRunStatusMsg formats the relative time once:

const relativeStatusToStringMap = {
  [TaskStatuses.COMPLETED]: 'taskFinishedRelativeStatus',
  [TaskStatuses.FAILED]: 'taskFailedRelativeStatus',
  [TaskStatuses.CANCELED]: 'taskCanceledRelativeStatus',
};

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still in place after the rebase — no code change this cycle. relativeStatusToStringMap (syncTaskUtils.js:143) maps status to string id, and getLastRunStatusMsg formats the relative time at the single call site.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No change this cycle — still the relativeStatusToStringMap status→string-id map at syncTaskUtils.js:143, with the relative time formatted once in getLastRunStatusMsg.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unchanged — still the relativeStatusToStringMap status→string-id map (syncTaskUtils.js:149), with the relative time formatted once in getLastRunStatusMsg.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No code change this cycle (CI diagnosis only). Still the relativeStatusToStringMap status→string-id map at packages/kolibri-common/utils/syncTaskUtils.js:149, with the relative time formatted once in getLastRunStatusMsg.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No change this cycle (CI-only work). Still the relativeStatusToStringMap status→string-id map at packages/kolibri-common/utils/syncTaskUtils.js:149, with the relative time formatted once in getLastRunStatusMsg.

);
}
const syncStep = syncTaskStatusToStepMap[task.extra_metadata.sync_state];
// extra_metadata carries over between runs, so a live run's sync_state can

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overly long comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cut to two lines: extra_metadata carries over between runs, so a live run still holds the previous run's sync_state until morango reports a step.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor follow-up: the QA fix in c6bb01d reworded this comment slightly, since the stale-sync_state reset now also covers a queued row that is due to run. Still two lines:

// extra_metadata carries over between runs, so a run that has not reported a
// step yet still holds the previous run's sync_state.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still two lines after the rebase — no code change this cycle (syncTaskUtils.js:194): extra_metadata carries over between runs, so a run that has not reported a step yet still holds the previous run's sync_state.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No change this cycle — the comment is still the two-line version at syncTaskUtils.js:194.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unchanged — still the two-line comment at syncTaskUtils.js:200.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No code change this cycle (CI diagnosis only). Still the two-line comment at packages/kolibri-common/utils/syncTaskUtils.js:200.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No change this cycle (CI-only work). Still the two-line comment at packages/kolibri-common/utils/syncTaskUtils.js:200.

@rtibblesbot

Copy link
Copy Markdown
Contributor Author

Addressed the review and force-pushed. Inline replies on each thread; the two structural ones:

  • taskStatusWithNextRun is deleted — the status and next-run clauses are joined with formatList (Intl.ListFormat) instead of a translated template. That drops the middot in favour of the locale's list separator; flagged on the thread in case you want it kept.
  • The syncSchedule fixture moved to packages/kolibri-common/utils/__tests__/ and all three specs now share it.

Commits: 9 → 6. The two trailing fixups are folded into the commits they fix, and the two storage commits are now one:

Record each finished run of a repeating job, and start the next one clean models + migration + storage.py + job.py
Expose the last finished run on the tasks API viewset
Show a repeating sync task's last run in the shared display logic kolibri-common utils + panel
Keep a repeating task listed on Device > Facilities device plugin
Stop the Facility > Data sync spinner when the run ends facility plugin
Show the last run of each schedule on Manage sync schedule ManageSyncSchedule.vue

The tree is byte-identical to the pre-recompose tree plus the review changes.

Verified: pytest kolibri/core/tasks/ (292 passed), pytest kolibri/core/auth/test/test_auth_tasks.py (48 passed), pnpm test-jest packages/kolibri-common kolibri/plugins/{device,facility,setup_wizard,user_profile} (1403 passed), prek run --all-files clean.


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

@rtibblesbot

rtibblesbot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

On the remaining part of the review — "if there is other unnecessary indirection in the code, that should be cleaned up too" — I went back over the diff for it and did not find anything further worth removing:

  • Each new export has a distinct consumer: taskDisplayStatus in FacilityTaskPanelDetails and SyncInterface, taskIsFinished in TasksBar, getLastRunStatusMsg in ManageSyncSchedule and in the joined status line.
  • Nothing stale is left: clearableTasks is gone with no callers, and the unused nextSync string is deleted.
  • showsLastRun does run twice in syncFacilityTaskDisplayInfo — directly, and again inside taskDisplayStatus. Collapsing that means inlining the body of taskDisplayStatus at the call site, which swaps a recomputation for duplicated knowledge, so I left it as is.

The three you named are all done; nothing new pushed for this pass.


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

@rtibbles rtibbles left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No further concerns from the code.

@pcenov

pcenov commented Jul 30, 2026

Copy link
Copy Markdown
Member

The issues observed in the latest build while manually testing:

  1. Staring a sync results in rerunning the same task in the Task manager instead of creating a new task:
same-task.mp4
  1. The Clear button is missing if the task is for a scheduled sync:
scheduled

After clicking the Clear completed button the scheduled completed tasks remain visible in the Task manager:

clear.completed.mp4

Logs: logs.zip

@pcenov

pcenov commented Jul 30, 2026

Copy link
Copy Markdown
Member

Forgot to mention that the Cancel button is also missing while a task is in progress.

@rtibblesbot

Copy link
Copy Markdown
Contributor Author

Thanks @pcenov — one real bug in there, which is fixed in c6bb01d. Taking them in turn:

1. Sync re-runs the same task instead of creating a new one

Two things bundled together here.

Re-using the row is intentional: #15052 gave the manual sync the same deterministic job_id as the schedule so the two dedup, and #15092 keeps that ("a manual sync landing on the existing schedule is the intended behaviour"). I checked the schedule survives it — interval, repeat and retry_interval are carried through _bring_scheduled_sync_forward, so pressing Sync brings the next run forward rather than replacing the schedule with a one-off.

But what the panel said while that happened was wrong, and that is what the video shows. Between pressing Sync and the runner picking the job up, the row still read:

Finished 4 minutes ago, next sync in 0 seconds

It was announcing the previous run plus a nonsense countdown, because the row is re-queued and my code only treated RUNNING/finished rows as describing a run of their own. A queued row whose scheduled time has passed is waiting to run now, so it now reads Waiting and then moves into the step-by-step progress. Fixed with a test.

2. No Clear on a scheduled sync, and Clear completed leaves them

This one is deliberate — it is an acceptance criterion on #15092:

The entry offers neither Clear nor Retry, and "Clear completed" neither removes it nor deletes the schedule.

For a repeating sync the task row is the schedule; there is no separate record. Clearing it would delete the schedule, so the daily sync would silently stop. The one-off Import 'MacKDPtest727' row at the top of your screenshot still has CLEAR, which is the intended split.

If the CLEAR COMPLETED button looking like it did nothing is itself the problem, that's fair — but the fix would be to the button's wording/visibility rather than to make it delete schedules. Say the word and I'll open a separate issue.

3. No Cancel while a task is in progress

Pre-existing, not from this PR. dataportalsync and peerfacilitysync are both registered with cancellable=False (kolibri/core/auth/tasks.py), and have been since 2022 — I confirmed the same value in v0.17.0, v0.18.0, and on the commit immediately before #15052. The API therefore reports cancellable: false and the panel has never offered Cancel for a facility sync. Nothing in this branch touches it.

Whether facility syncs should be cancellable is a real question, but it's a backend change (morango has to be able to unwind a part-finished transfer session) and belongs in its own issue rather than here. Happy to file it.

While I was in there I did tighten the related guard: Cancel is now only offered for a genuinely live run, because cancelling a queued repeating row clears the job — which would delete the schedule.


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

@rtibblesbot

Copy link
Copy Markdown
Contributor Author

CI note: the only red check is Android smoke test (API 24), which is an infra hang, not a change here. The job stuck on adb install -r platforms/android/dist/*.apk at 23:12 and was cancelled at the 6h job limit; API 30 and API 35 installed and smoke-tested the same APK in ~3 min, and API 24 passed in ~3 min on another PR built from the same develop base an hour earlier. This branch touches no Android code. A re-run should clear it.

@rtibblesbot
rtibblesbot force-pushed the issue-15092-bb7f7f branch 3 times, most recently from 0f898f4 to 42a8398 Compare July 31, 2026 07:46
rtibblesbot and others added 3 commits July 31, 2026 01:15
…lean

A repeating job is re-queued the instant a run ends, so its own state
stops describing that run and the task manager loses the only record of
what the user just watched. Stamp the outcome onto two dedicated columns
on every transition into a finished state, which covers all three exits
- recurrence, failure retry and a task-requested retry_in delay - in the
same write that ends the run, before any reschedule re-queues the row.

A job that is not re-scheduled stamps the same columns with its own
finished state, so the snapshot never contradicts the row it sits on.

progress, total_progress, exception, traceback and result live in
saved_job and survive the reschedule, so a second run would otherwise
inherit the previous run's progress bar and exception. They have to
survive until the next run actually starts - until then the queued row
is still describing the run that just ended - so reset them on the
transition into RUNNING rather than at reschedule time. A bare re-mark
of an already-RUNNING job is not a new run and stays a no-op.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
last_finished_status and last_finished_datetime mirror the existing
state -> status and scheduled_time -> scheduled_datetime naming.

clearable stays state-derived, so a re-scheduled recurring row is not
clearable and "Clear completed" cannot delete a sync schedule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A re-queued schedule is QUEUED, so the panel rendered a full progress bar
and inferred "is running" from extra_metadata.sync_state, which is stale
(COMPLETED) on that row. Centralise "which run is being shown" in
syncTaskUtils so the panel, the tasks bar and the schedule table agree:

  Finished 2 minutes ago, next sync in 1 day
  Failed 5 minutes ago, retrying in 1 hour

The two clauses are joined with formatList so the separator and the
ordering follow the locale rather than a hard-coded string. A failed run
is only announced as retrying when the schedule has a retry_interval;
without one it re-schedules onto its ordinary recurrence.

The same staleness makes a second automatic run read as finished until
morango reports its first step, because a recurrence re-persists the
previous run's extra_metadata; treat a terminal sync_state as PENDING,
which is what a fresh enqueue writes.

Neither cancel nor retry is offered on a re-queued schedule: cancelling a
QUEUED job clears it, which would delete the schedule, and the schedule
already retries itself.

The unused nextSync string goes — the next-run clause now lives in
TaskStrings, where the panel and the schedule table both reach it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rtibblesbot and others added 4 commits July 31, 2026 01:15
The task manager hid every repeat === null row that was not RUNNING, so a
manual KDP sync vanished the instant it finished. List one that has a
last finished run too; a schedule that has never run stays out, because
it belongs on Manage sync schedule.

With recurring rows always present, "not clearable" stops being a proxy
for "syncing" — it would pin every registered facility to a permanent
spinner, and it would leave the tasks bar reading "0 of 1 complete"
forever. Both now go off the run being displayed.

The empty state tested facilityTasks rather than the filtered list, so a
device whose only task was a hidden schedule rendered neither tasks nor
the "no tasks" message.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SyncInterface polled a single task until it became clearable, which a
repeating row never does — so after a manual KDP sync from this page the
row spun forever and the 2-second poll never stopped. It also cleared
that id on completion, which for a schedule would delete the schedule.

Poll against the last finished run recorded when the sync was started
instead, and only clear a task that is genuinely one-off. That baseline
lives until the next sync starts: clearing it on teardown made an
in-flight poll response read as "run finished" against a null baseline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An empty placeholder stands in for a schedule that has never run.

The empty-state cell was already under-spanning its four columns; with
the new one it spans five.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A server that fails to start left the ones before it running and holding
open connections to their test databases, so every later test failed to
create its own.
@pcenov

pcenov commented Jul 31, 2026

Copy link
Copy Markdown
Member

Thanks @rtibblesbot, manually tested and confirmed that the acceptance criteria is met in the latest build so that:

  • Sync re-runs the same task instead of creating a new one
  • There is no Clear on a scheduled sync, and clicking Clear completed leaves them
  • There is no Cancel while a task is in progress

@rtibbles rtibbles left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code changes check out, manual QA passes.

The concerns about new syncs not appearing as new tasks we will carry into follow up issues, but not needed here.

@rtibbles
rtibbles merged commit aeb06bf into learningequality:develop Jul 31, 2026
98 checks passed
@rtibblesbot
rtibblesbot deleted the issue-15092-bb7f7f branch July 31, 2026 15:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

APP: Device Re: Device App (content import/export, facility-syncing, user permissions, etc.) APP: Facility Re: Facility App (user/class management, facility settings, csv import/export, etc.) DEV: backend Python, databases, networking, filesystem... DEV: frontend SIZE: large

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Repeating tasks disappear from the task manager once a run finishes

3 participants