Skip to content

SEP-1759: Paginate the periodic-tasks list endpoint and its SEP consumers - #1303

Open
peter-o-addo wants to merge 7 commits into
mainfrom
SEP-1759
Open

SEP-1759: Paginate the periodic-tasks list endpoint and its SEP consumers#1303
peter-o-addo wants to merge 7 commits into
mainfrom
SEP-1759

Conversation

@peter-o-addo

@peter-o-addo peter-o-addo commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Paginate the periodic-tasks list end-to-end so unbounded schedule responses become a PaginatedResponse envelope and every SEP consumer still sees the full set.

  • app/tasks/periodic/crud.py, app/tasks/periodic/routes.py: add stable list ordering and return a paginated /periodic/ page on both filter branches
  • app/sep/api/routes/periodic_tasks.py, app/sep/deps.py: forward offset/limit through the SEP proxy and walk all pages for server-rendered task views
  • frontend/packages/framework/.../ScheduledTasksPanel/hooks.ts, frontend/packages/api/src/hooks/useAppTasks.ts (+ package exports): walk all periodic-task pages before client-side plugin filtering, and export the shared page-cap helpers
  • frontend/packages/api/specs/{tasks,sep}.json, frontend/packages/api/src/generated/{tasks,sep}.ts, changelog.d/SEP-1759.changed.md: regenerate OpenAPI clients for the new envelope and record the changelog
  • Tests updated for pagination envelopes, page walks, proxy forwarding, and React mock compatibility

Tested

  • GET /api/tasks/periodic/?offset=0&limit=2 returns a PaginatedResponse envelope (items, total, offset, limit) and len(items) <= limit
  • Same offset/limit query twice returns a stable id window ([17, 25])
  • Seed to total=5, walk with limit=2 — unique ids, sorted ascending, walked == total
  • GET /api/tasks/periodic/?enabled=true&offset=0&limit=2 returns only enabled rows in the paginated envelope
  • Owner filter: owner=ALTERS → empty envelope; owner=BACKUPS (after seed) → filtered page (total=4, ids [25, 35])
  • SEP proxy matches upstream for the same window (baseline and page 2 after seed)

Checklist

  • New/modified functions have type hints and rST docstrings
  • New tests added for new features or bug fixes
  • All tests pass locally (make test)
  • Pre-commit hooks pass (make run-pre-commit)
  • Database migrations generated if models changed (make makemigrations)
  • User-facing changes documented (README, inline help, UI text)
  • Configuration changes documented with examples
  • Changelog fragment added under changelog.d/ if the change is user-facing (make changelog-add), or confirmed N/A (internal-only change, or a same-release-cycle fix for an unreleased sibling ticket)

@peter-o-addo peter-o-addo self-assigned this Aug 7, 2026
@peter-o-addo
peter-o-addo marked this pull request as ready for review August 7, 2026 12:58
Copilot AI review requested due to automatic review settings August 7, 2026 12:58
@peter-o-addo peter-o-addo added the qa in progress Someone is currently testing this PR - do not merge it label Aug 7, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces end-to-end pagination for the Tasks periodic-tasks list (/api/tasks/periodic/) and its SEP gateway proxy (/api/sep/periodic-tasks/), updating both server-rendered pages and React consumers to walk all pages so schedules aren’t truncated at the default page size.

Changes:

  • Tasks periodic list now returns a PaginatedResponse envelope and uses deterministic ordering to make offset/limit paging stable.
  • SEP backend consumers forward pagination params upstream and walk all pages where a full set is required (server-rendered views).
  • Frontend consumers (ScheduledTasksPanel and API hooks) normalize and walk paginated list responses; OpenAPI specs/clients and tests are updated accordingly.

Reviewed changes

Copilot reviewed 17 out of 19 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
app/tasks/periodic/routes.py Switch periodic list route to return a PaginatedResponse and accept pagination dependency.
app/tasks/periodic/crud.py Add stable default ordering for PeriodicTask listing to support deterministic paging.
app/sep/api/routes/periodic_tasks.py Forward offset/limit through the SEP proxy and validate/coerce the upstream paginated envelope.
app/sep/deps.py Walk all pages from Tasks periodic list for server-rendered contexts that need the full set.
tests/app/tasks/periodic/test_routes.py Update route tests to assert paginated envelopes and add paging determinism/page-walk coverage.
tests/app/tasks/periodic/test_crud.py Add manager-level ordering/pagination determinism tests.
tests/app/sep/api/routes/test_periodic_tasks.py Update SEP proxy tests to assert paginated envelope forwarding and defaults.
frontend/packages/framework/src/components/ScheduledTasksPanel/hooks.ts Walk all periodic-task pages client-side before filtering schedules to a plugin.
frontend/packages/framework/src/components/ScheduledTasksPanel/ScheduledTasksPanel.test.tsx Add tests for page-walking behavior and array short-circuit behavior.
frontend/packages/framework/src/components/SchemaDrivenApp/AppDetailPage.test.tsx Adjust @sep/api mocks to satisfy new pagination helpers/constants usage.
frontend/packages/apps/inventory/src/InventorySchedulePage.test.tsx Adjust @sep/api mock pattern for compatibility with expanded exports.
frontend/packages/api/src/hooks/useAppTasks.ts Export shared “fetch all pages” helper and document its behavior/caps.
frontend/packages/api/src/hooks/index.ts Re-export pagination helpers/constants from hooks entrypoint.
frontend/packages/api/src/index.ts Re-export pagination helpers/constants from package root.
frontend/packages/api/specs/tasks.json Regenerate Tasks OpenAPI spec for paginated periodic list response + query params.
frontend/packages/api/specs/sep.json Regenerate SEP OpenAPI spec for paginated proxy response + query params.
frontend/packages/api/src/generated/tasks.ts Regenerate TS client types for paginated periodic list endpoint.
frontend/packages/api/src/generated/sep.ts Regenerate TS client types for paginated SEP proxy endpoint.
changelog.d/SEP-1759.changed.md Add changelog entry describing the pagination envelope and page-walking behavior.

Comment on lines +43 to +48
:cvar ordering: The default ordering for listing periodic tasks. `PeriodicTask`
is not a `BaseSQLModel`, so `BaseManager._get_ordering()` has no
`created_at` fallback to offer and would leave SELECTs unordered, making
offset pagination undefined. The primary key is unique, so ordering by it
alone is total and needs no tie-breaker. Business-meaningful ordering is
SEP-304.
enabled: bool | None = None,
) -> list[PeriodicTask]:
"""List all periodic tasks."""
) -> PaginatedResponse[PeriodicTaskResponse]:
Comment on lines +43 to +51
/**
* Walk every page of the SEP periodic-task list before client-side filtering.
*
* Mirrors ``fetchAllAppListPages`` so a large schedule table cannot hide this
* plugin's rows past the first page. Bare-array responses (stories/tests)
* short-circuit after one request; hitting the page cap surfaces a warning
* rather than silently dropping schedules.
*/
async function fetchAllPeriodicTasks(): Promise<PeriodicTaskResponse[]> {
@peter-o-addo peter-o-addo added qa passed Tests for this PR are completed and successful. and removed qa in progress Someone is currently testing this PR - do not merge it labels Aug 7, 2026
@peter-o-addo

Copy link
Copy Markdown
Contributor Author

1. Paginated envelope + page size

GET /api/tasks/periodic/?offset=0&limit=2

{
  "items": [
    {
      "name": "run_test-mydumpers_every_5_days_-1940910910313412493",
      "task": "test-mydumpers",
      "start_time": null,
      "enabled": true,
      "description": "",
      "execute_request": {
        "meta": {},
        "payload": null,
        "eta": null,
        "anonymize_mask": null,
        "chain_task_names": [
          "test-mydumper-secondary"
        ],
        "chain_on_failure": false
      },
      "interval": {
        "every": 5,
        "period": "days"
      },
      "crontab": null,
      "id": 17,
      "last_run_at": null,
      "last_run_status": null,
      "total_run_count": 0,
      "date_changed": "2026-05-20T11:45:00.162865Z",
      "period": "every 5 days",
      "next_run_at": "2026-08-12T10:48:50.222178Z"
    },
    {
      "name": "run_test-mydumper_every_1_hour_-326798780279067240",
      "task": "test-mydumper",
      "start_time": null,
      "enabled": true,
      "description": "",
      "execute_request": null,
      "interval": {
        "every": 1,
        "period": "hours"
      },
      "crontab": null,
      "id": 25,
      "last_run_at": null,
      "last_run_status": null,
      "total_run_count": 0,
      "date_changed": "2026-07-21T15:08:57.552856Z",
      "period": "every 1 hour",
      "next_run_at": "2026-08-07T11:48:50.222187Z"
    }
  ],
  "total": 2,
  "offset": 0,
  "limit": 2
}

2. Stable window (same query twice)

GET /api/tasks/periodic/?offset=0&limit=2 (repeated — identical id windows)

Call 1

{
  "items": [
    {
      "id": 17,
      "name": "run_test-mydumpers_every_5_days_-1940910910313412493",
      "task": "test-mydumpers",
      "enabled": true,
      "last_run_status": null
    },
    {
      "id": 25,
      "name": "run_test-mydumper_every_1_hour_-326798780279067240",
      "task": "test-mydumper",
      "enabled": true,
      "last_run_status": null
    }
  ],
  "total": 2,
  "offset": 0,
  "limit": 2
}

Call 2

{
  "items": [
    {
      "id": 17,
      "name": "run_test-mydumpers_every_5_days_-1940910910313412493",
      "task": "test-mydumpers",
      "enabled": true,
      "last_run_status": null
    },
    {
      "id": 25,
      "name": "run_test-mydumper_every_1_hour_-326798780279067240",
      "task": "test-mydumper",
      "enabled": true,
      "last_run_status": null
    }
  ],
  "total": 2,
  "offset": 0,
  "limit": 2
}

3. Multi-page walk (seeded to total=5, limit=2)

3a. Seed temporary schedules

POST /api/tasks/test-mydumper/periodic/

{
  "name": "manual-page-test-0",
  "enabled": true,
  "description": "temp pagination probe",
  "interval": { "every": 30, "period": "minutes" },
  "crontab": null,
  "start_time": null,
  "execute_request": null
}

Created (201 each):

{ "id": 35, "name": "manual-page-test-0", "task": "test-mydumper", "enabled": true }
{ "id": 36, "name": "manual-page-test-1", "task": "test-mydumper", "enabled": true }
{ "id": 37, "name": "manual-page-test-2", "task": "test-mydumper", "enabled": true }

3b. Walk pages

Unique ids, sorted ascending, walked == total (5).

GET /api/tasks/periodic/?offset=0&limit=2

{
  "items": [
    {
      "id": 17,
      "name": "run_test-mydumpers_every_5_days_-1940910910313412493",
      "task": "test-mydumpers",
      "enabled": true,
      "last_run_status": null
    },
    {
      "id": 25,
      "name": "run_test-mydumper_every_1_hour_-326798780279067240",
      "task": "test-mydumper",
      "enabled": true,
      "last_run_status": null
    }
  ],
  "total": 5,
  "offset": 0,
  "limit": 2
}

GET /api/tasks/periodic/?offset=2&limit=2

{
  "items": [
    {
      "id": 35,
      "name": "manual-page-test-0",
      "task": "test-mydumper",
      "enabled": true,
      "last_run_status": null
    },
    {
      "id": 36,
      "name": "manual-page-test-1",
      "task": "test-mydumper",
      "enabled": true,
      "last_run_status": null
    }
  ],
  "total": 5,
  "offset": 2,
  "limit": 2
}

GET /api/tasks/periodic/?offset=4&limit=2

{
  "items": [
    {
      "id": 37,
      "name": "manual-page-test-2",
      "task": "test-mydumper",
      "enabled": true,
      "last_run_status": null
    }
  ],
  "total": 5,
  "offset": 4,
  "limit": 2
}

3c. Cleanup

DELETE /api/tasks/periodic/35 → 204
DELETE /api/tasks/periodic/36 → 204
DELETE /api/tasks/periodic/37 → 204

4. enabled=true filter

GET /api/tasks/periodic/?enabled=true&offset=0&limit=2

{
  "items": [
    {
      "id": 17,
      "name": "run_test-mydumpers_every_5_days_-1940910910313412493",
      "task": "test-mydumpers",
      "enabled": true,
      "last_run_status": null
    },
    {
      "id": 25,
      "name": "run_test-mydumper_every_1_hour_-326798780279067240",
      "task": "test-mydumper",
      "enabled": true,
      "last_run_status": null
    }
  ],
  "total": 2,
  "offset": 0,
  "limit": 2
}

5. Owner filter

5a. Owner with no schedules (ALTERS)

GET /api/tasks/periodic/?owner=ALTERS&offset=0&limit=2

{
  "items": [],
  "total": 0,
  "offset": 0,
  "limit": 2
}

5b. Owner with schedules (BACKUPS, after seed)

GET /api/tasks/periodic/?owner=BACKUPS&offset=0&limit=2

{
  "items": [
    {
      "id": 25,
      "name": "run_test-mydumper_every_1_hour_-326798780279067240",
      "task": "test-mydumper",
      "enabled": true,
      "last_run_status": null
    },
    {
      "id": 35,
      "name": "manual-page-test-0",
      "task": "test-mydumper",
      "enabled": true,
      "last_run_status": null
    }
  ],
  "total": 4,
  "offset": 0,
  "limit": 2
}

6. SEP proxy matches upstream

6a. Baseline window

Upstream GET /api/tasks/periodic/?offset=0&limit=2

{
  "items": [
    {
      "id": 17,
      "name": "run_test-mydumpers_every_5_days_-1940910910313412493",
      "task": "test-mydumpers",
      "enabled": true,
      "last_run_status": null
    },
    {
      "id": 25,
      "name": "run_test-mydumper_every_1_hour_-326798780279067240",
      "task": "test-mydumper",
      "enabled": true,
      "last_run_status": null
    }
  ],
  "total": 2,
  "offset": 0,
  "limit": 2
}

Proxy GET /api/sep/periodic-tasks/?offset=0&limit=2

{
  "items": [
    {
      "id": 17,
      "name": "run_test-mydumpers_every_5_days_-1940910910313412493",
      "task": "test-mydumpers",
      "enabled": true,
      "last_run_status": null
    },
    {
      "id": 25,
      "name": "run_test-mydumper_every_1_hour_-326798780279067240",
      "task": "test-mydumper",
      "enabled": true,
      "last_run_status": null
    }
  ],
  "total": 2,
  "offset": 0,
  "limit": 2
}

6b. Page 2 after seed (offset=2&limit=2)

Upstream

{
  "items": [
    {
      "id": 35,
      "name": "manual-page-test-0",
      "task": "test-mydumper",
      "enabled": true,
      "last_run_status": null
    },
    {
      "id": 36,
      "name": "manual-page-test-1",
      "task": "test-mydumper",
      "enabled": true,
      "last_run_status": null
    }
  ],
  "total": 5,
  "offset": 2,
  "limit": 2
}

Proxy

{
  "items": [
    {
      "id": 35,
      "name": "manual-page-test-0",
      "task": "test-mydumper",
      "enabled": true,
      "last_run_status": null
    },
    {
      "id": 36,
      "name": "manual-page-test-1",
      "task": "test-mydumper",
      "enabled": true,
      "last_run_status": null
    }
  ],
  "total": 5,
  "offset": 2,
  "limit": 2
}

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  app/sep
  inventory.py
  app/sep/api/routes
  periodic_tasks.py
  app/tasks/periodic
  crud.py
  routes.py 80
Project Total  

This report was generated by python-coverage-comment-action

@github-actions github-actions Bot added app:inventory PR touches the inventory app slice svc:tasks PR touches the tasks service (app/tasks/) labels Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app:inventory PR touches the inventory app slice frontend python qa passed Tests for this PR are completed and successful. svc:tasks PR touches the tasks service (app/tasks/)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants