Task: Schedule a report from its own page
Table of Contents
This page documents a task in the Rework the market data feeds into a market data service with adapters — the remaining work story. It captures the goal, current status, acceptance, and any notes or results.
1. Goal
A person with the permission schedules or unschedules a report from its page, and activates an on-demand report by giving it a schedule.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Rework the market data feeds into a market data service with adapters — the remaining work |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-11-04 |
3. Acceptance
- The report page offers Schedule and Unschedule, gated on the permission, calling the existing operations.
- An on-demand report can be activated: a real schedule is set and scheduled, and deactivated back to on demand.
- The screen states the outcome and the tests are green.
4. Plan
- Read the operation, not a new one:
schedule_report_definitionsandunschedule_report_definitionsalready carry the ids, the count, the failed ids and the words, and the shell already drives them. The task is the app action, so the BFF relays those operations and the page states their answers. - Put the permission on the route as the operation states it. The handler checks the write of a definition to schedule and its delete to unschedule, and the page's two buttons follow the same pair, so a person who may change a definition is not offered a way to take its schedule away.
- Activate an on-demand report with one action rather than two forms. The scheduler reads the expression from the definition, so the expression has to be stored before the schedule is made; the BFF does both in one call, at the version it read, so the page never asks a person to save and then schedule.
- Let the list and the page agree on one word. A definition with no scheduler
job is on demand whatever expression it keeps, and the expression that never
fires is on demand as well, so both read
isOnDemandrather than a rule each. - Do not hide a refusal. The operation answers a refusal as a result, so the page reads the count and states nothing changed, or states the server's reason as an error; no toast says success for a write that did not happen.
5. Measurements
Every number this task claims about a tree, with the command that produced it
and the commit it was measured at. A number without both is not a measurement,
because a clean-up record describes a tree and trunk moves under a branch: a
consumer count, a gate's component count and a suite's size all change with no
code changing, and the reader of a bare number cannot tell which tree it came
from. Where the raw output matters, put it in an evidence/ file beside the
task and name that file in the Command column.
| What | Value | Command | Commit |
|---|---|---|---|
| BFF reporting tests | 50 passing | npx vitest run packages/bff/src/reporting.test.ts |
ecc2d2fd |
| Web reporting tests | 53 passing, 6 in the client | npx vitest run packages/web/src/reporting |
ecc2d2fd |
| Definitions the demo tenant holds | 172 | select count(*) from (select distinct on (id) id from ores_reporting_report_definitions_tbl where tenant_id'482df22b-ee2c-4482-b741-ea937154a2f7' order by id, version desc) x= |
e8397c82 |
| Report jobs the scheduler holds live before and after the checks | 20 | select count(*) from ores_scheduler_job_definitions_tbl where job_name like 'report_definition.%' and valid_to'9999-12-31 23:59:59+00'= |
e8397c82 |
6. Notes
7. Test Scenarios
Manual QA scenarios (scaffolded via compass add test_scenario)
that verify this task. Link
new ones here as they're created; the scenario doc itself links back
via its "Verifies task" field.
| Scenario | State | Notes |
|---|---|---|
8. PRs
| PR | Title |
|---|---|
| #4454 | [reporting] Offer Schedule and Unschedule on a report's page |
9. Review
| Comment summary | File | Decision | Notes |
|---|---|---|---|
10. Result
A person who holds the permission schedules or unschedules a report from the report's own page, and activates an on-demand report by stating the schedule it runs on.
10.1. The routes
| Route | Operation | Permission |
|---|---|---|
POST /api/reporting/reports/schedule |
reporting.v1.ops.schedule_report_definitions |
reporting::report_definitions:write |
POST /api/reporting/reports/unschedule |
reporting.v1.ops.unschedule_report_definitions |
reporting::report_definitions:delete |
Each takes {ids: [...]} and answers {changed, failedIds, message}, which is
the operation's own count, its own failed ids and its own words. The person's
session makes the call, so the service reads only the definitions that session
may see and acts for the party that owns them; a definition of another party is
refused in the operation's words. The permission the route gates on is the one
the operation checks, read before the route writes, because a schedule writes
the expression the job will fire on.
The schedule route takes an optional schedule_expression, which names one
definition. The scheduler builds a job from the expression the definition holds,
so activating an on-demand report writes that expression first, at the version
the route read, and then schedules it. A refusal of the operation is an answer
and not an error: the page reads the count and the words, and a refusal it does
not hide.
10.2. The page actions
The report page draws Schedule on a definition with no scheduler job and
Unschedule on one that has, each gated on the permission its own operation
checks. Neither is offered when the page can tell that the definition's party is
not the session's, and the page then says whose report it is and which party to
act for.
Schedule opens a dialog over the existing CronField and builder, so a person
who does not know cron states the schedule in controls and one who does opens
the expression. The expression and the schedule are one action. Unschedule
withdraws the schedule and leaves the report on demand; the run grant stays,
because consent to run is the report's and not the schedule's.
Every outcome is in words above the tabs: "Scheduled", "Nothing changed. The report already had a schedule", "Unscheduled", "Nothing changed. The report had no schedule to remove", and, for a refusal, "The server refused this change:" followed by the operation's own reason. A count of zero reads as nothing changed and never as a success.
10.3. On demand
A definition reads as on demand when it holds no scheduler job, which is the
fact that says nothing will fire, and when the expression it holds is the one
that never fires (0 0 31 2 *). The list's Schedule column and the page's "When
it runs" detail read the same rule from isOnDemand, so the two cannot disagree
about the word.
10.4. What was verified
- The BFF: the ids the operation is called with, the expression the write carries and the version it holds, a refusal surfaced as the answer, both permission gates and the refusal of an expression that names two definitions.
- The page: the actions offered by permission, the delete gate that keeps
Unschedulefrom a person who may only write, an on-demand report and a scheduled one, and every outcome in words. - Live, in the demo tenant: an on-demand report was activated from its page and
unscheduled again, and the scheduler job, the definition's
scheduler_job_idand the IAM run grant were read from the services between the two. The party guard was seen on a report of another party. The evidence is evidence/report_schedule_live.txt. - The harness check
report-schedule.spec.tsdrives the same round trip and leaves the report as it found it.
What could not be verified: that a scheduled report fires. The checks leave the demo tenant as they found it, so the evidence that a schedule will fire is the scheduler's live job row with its expression and not a run the schedule produced.