Skip to content

Add scheduled reports and a pluggable distribution SPI - #95

Merged
royteeuwen merged 16 commits into
mainfrom
feature/scheduled-reports
Jul 21, 2026
Merged

Add scheduled reports and a pluggable distribution SPI#95
royteeuwen merged 16 commits into
mainfrom
feature/scheduled-reports

Conversation

@royteeuwen

Copy link
Copy Markdown
Contributor

Summary

Reports can now run unattended on a cron schedule and deliver their results through a pluggable distribution SPI, available for both scheduled and manual runs.

Scheduling

  • Per-report cron schedule on the definition, reconciled to a Sling scheduled job (persistent, cluster-safe) on save/delete.
  • Cron expressions are validated before persistence (Quartz 6/7-field form); sub-minute schedules are rejected.
  • Run-as identity, two paths:
    • UI/API schedules run as their authorrunAs/scheduledBy are forced server-side to the requesting user (body values ignored), enabled by a self-scoped impersonation grant. A user can only ever schedule a report to run as themselves, so scheduling can never be used to gain access.
    • Code-deployed reports are auto-discovered from the immutable /apps/groovyconsole-reports-definitions drop-zone (startup scan + resource-change listener) and run as the dedicated aem-groovy-console-reports-executor service user (reads /content). Because /apps can't be written at runtime, this is the trust boundary for the executor identity.
  • Jobs are keyed by definition path, so /conf and /apps definitions never collide.

Distribution

  • ReportDistributor / ReportDistributorRegistry SPI mirroring the exporter SPI; distributors render through any registered export format (CSV, XLSX, …).
  • Email distributor (AEM mail service) with an optional recipient-domain allowlist (empty = send anywhere); Filesystem distributor sandboxed to a configured root via canonical-path check and disabled by default.
  • Targets stored on the definition, applied automatically on scheduled completion and on demand via Distribute now; a distribution failure is recorded on the execution (distributionErrors) but never fails the run.

Security notes

  • The trust boundary for authoring executable report scripts is JCR write access to /conf/groovyconsole/reports (unchanged); the console-permission check is defense-in-depth on top.
  • Forged runAs fails closed (impersonation is only ever self-granted); manual runs always use the caller's own resolver.

Verification

  • mvn clean install green across all modules (aemanalyser on).
  • 47 reports-bundle unit tests green.
  • 26/26 reports integration tests green on the Sling Starter.

Includes editor UI for schedules and distribution targets, and README documentation.

Reports can now run unattended on a cron schedule and deliver their
results through a pluggable distribution SPI, for both scheduled and
manual runs.

Scheduling
- Per-report cron schedule stored on the definition, reconciled to a
  Sling scheduled job (persistent, cluster-safe) on save/delete.
- Cron expressions are validated before persistence (Quartz 6/7-field
  form); sub-minute schedules are rejected.
- UI/API schedules run as their author (runAs/scheduledBy forced
  server-side to the requesting user, enabled by a self-scoped
  impersonation grant) so scheduling can never escalate privileges.
- Reports deployed in code are auto-discovered from the immutable
  /apps/groovyconsole-reports-definitions drop-zone (startup scan plus a
  resource-change listener) and run as the dedicated executor service
  user, which reads /content. Jobs are keyed by definition path.

Distribution
- ReportDistributor/ReportDistributorRegistry SPI mirroring the exporter
  SPI; distributors render through any registered export format.
- Email distributor (AEM mail service) with an optional recipient-domain
  allowlist; filesystem distributor sandboxed to a configured root and
  disabled by default.
- Targets stored on the definition, applied automatically on scheduled
  completion and on demand via "Distribute now"; failures are recorded
  on the execution without failing the run.

Includes editor UI for schedules and distribution targets, README
documentation, unit tests and integration tests.
The console.api package and the reports api/model packages are still on
the unreleased 20.0.0 line (baseline 19.1.0), so additive changes don't
need a package-version bump. Restore them to 20.0.0 / 1.0.0 and describe
the /apps definitions drop-zone by what it is.
The schedule editor now offers a Frequency picker (every day / weekday /
week / month / custom) with hour, minute, weekday and day-of-month
inputs, a plain-language summary and the resulting cron expression. The
raw cron field remains under "Custom", with the same client-side
validation the server applies. Existing expressions are parsed back into
the matching preset.

Each distribution field now has a visible label and help text
(Destination, Export format, Directory, File name, Recipients, Subject)
instead of relying on placeholders, and each target is shown as its own
card.
royteeuwen added a commit that referenced this pull request Jul 20, 2026
…ge to main

Records the refreshed 19.x history as merged (so future 20.x->19.x fix syncs have a clean
base) while keeping main's tree verbatim: Groovy 5, modern UI and 20.x package exports are all
preserved. -s ours = none of 19.x's downgrades land on main; #95 reaches main via its own PR.
Add ReportDistributor.isAvailable() and list only available distributors
in the distributors endpoint, so the editor never offers a destination
that would fail. The filesystem distributor is available only when
enabled; the email distributor only when a mail service is bound.
The page servlet appends a per-deploy version token (the bundle's
last-modified time) to the stable-named entry assets, and the reports
Vite build now content-hashes shared chunks. The entry is refetched
after a deploy and pulls in the hashed chunks, so a redeploy shows up
without clearing the browser cache, while assets stay cached within a
deployment.
…duled-reports

# Conflicts:
#	extensions/reports/bundle/pom.xml
#	extensions/reports/bundle/src/main/groovy/be/orbinson/aem/groovy/console/reports/servlets/ReportsServlet.groovy
#	extensions/reports/ui.frontend/src/api/reports-api.ts
#	extensions/reports/ui.frontend/src/api/reports-types.ts
#	extensions/reports/ui.frontend/src/components/reports/gcr-report-editor.ts
#	it.tests/src/test/java/be/orbinson/aem/groovy/console/it/GroovyConsoleReportsIT.java
Align testDistributorsListed with the availability gating: on the plain
Sling test instance the filesystem distributor is disabled and email has
no mail service, so the endpoint lists neither. Also remove a PathType
import left unused after merging the query-audit browse changes.
… test

The per-deploy ?v token on the module entry made the browser load
reports.js twice (once as reports.js?v=…, once as the bare specifier the
bundle's own imports use), so sp-theme was defined twice and the second
define threw — aborting the dynamic import chain and leaving the Monaco
code editor unregistered on any report whose editor also lazy-loads the
path browser (e.g. a PATH parameter). Revert the ?v token and the chunk
hashing back to the known-good stable-name build.

Add a Playwright test that opens a report's edit view and asserts the
Monaco editor actually renders, closing the gap that let this through
(the existing reports specs only cover the list and run views).
Content-hash the reports entry, chunks and assets, emit a Vite manifest,
and have the shared page servlet link the hashed entry (js + css) read
from that manifest. A changed file gets a fresh URL and is refetched; an
unchanged file keeps its URL and stays cached — busting without a query
token, which previously double-loaded the entry module and broke the
code editor. The console-panel entry keeps a stable name (the extension
provider references it by fixed path), and the servlet falls back to the
stable <name>.js/.css names when no manifest is present (e.g. migration).
The migration service serialises runs and rejects a new run with 409
while another is in progress or queued. Other migration tests in the
suite can still be draining an async run when this test fires, so its
single POST intermittently got a 409 (surfacing as "no response from
migration run"). Retry the run until it is accepted, and mark the
audited script ".always." so the accepted run re-executes and audits it
even if a background run executed it first (which would otherwise skip
it and drop the queryAudit).
Extend the manifest-based cache-busting to all console SPAs, so every
content-hashed asset is linked by its hashed URL (cached while unchanged,
refetched on change) with a graceful fallback to stable names:

- add SpaManifest, a shared resolver (entry js/css by chunk name, and
  stable-module-URL -> hashed) used across the page servlet and console;
- core modern console: hash the entry + chunks + assets and resolve the
  entry js, entry css and Monaco chunk css via ModernConsoleConfig, which
  the HTL page now links (also resolves the extension panel module URLs);
- reports: also hash the console-panel entry (was stable);
- migration: hash entries/chunks/assets + emit a manifest, so the shared
  servlet busts the migration page and the console busts its panel.

Tests: it.tests assert hashed names for the reports page, reports panel,
core entry, migration page and migration panel; the Playwright specs load
every page (incl. the reports edit-view Monaco guard) so a manifest/
hashing regression that breaks a page is caught.
- The split Run button applied left-only rounding to the Run segment
  unconditionally, so without the (extension-registered) chevron the
  button looked square on the right. Default to a full pill; square the
  right edge only when the split chevron is present.
- The reports/migration ui.apps nav filters replaced the shared
  /apps/cq/core/content/nav tree on install, wiping the core console's
  "AEM Groovy Console" Tools entry (and each other's). Use mode="merge"
  on all three nav filters so each adds its entry without deleting
  siblings.
Scheduler fixed-parameter values now render the same rich typed widgets as
the run form (SELECT/DYNAMIC dropdowns, PATH/TAG browser, DATE picker, BOOLEAN
checkbox, and a multifield for 'multiple' parameters) via a shared
gcr-parameter-fields component, replacing the plain text inputs. The schedule
parameterValues model now carries arrays end-to-end (ReportSchedule Map<String,
Object>, servlet + service preserve List values, TS Record<string,
ReportParameterValue>).

Nav overlay filters switched to a two-filter pattern (merge shared nav folders,
own only the leaf entry) so each package creates its Tools entry without
wiping siblings.
…g is ready

The migration scripts base under /conf is provisioned asynchronously and can
still be absent when the console endpoint first reports ready, so the one-shot
deploy in setUp intermittently 404s. Wrap it in an awaitility retry.
Two new flags on the reports configuration service (both default on):
schedulingEnabled and distributionEnabled.

Backend enforcement:
- scheduling: DefaultReportScheduleService skips registration and unschedules
  all report jobs when disabled; ReportScheduledJobConsumer skips any job that
  still fires (authoritative runtime guard).
- distribution: applyDistributions is the single sink for both scheduled and
  manual runs, so one guard there blocks every distribution; the manual
  ReportDistributeServlet returns 503 with a clear message.

Frontend adapts: a new GET /bin/groovyconsole/reports/config endpoint exposes
the flags; the editor hides the Schedule / Distribution sections and the run
view hides 'Distribute now' when the matching feature is off (defaulting to
enabled if the endpoint is unavailable).
@sonarqubecloud

Copy link
Copy Markdown

@royteeuwen
royteeuwen merged commit 7a10156 into main Jul 21, 2026
3 checks passed
royteeuwen added a commit that referenced this pull request Jul 21, 2026
…ge to main

Refreshed after PR #95 (scheduled reports) merged to main. -s ours keeps main's tree
verbatim (Groovy 5, modern UI, 20.x packages); records the 19.x history as merged so
future 20.x->19.x fix syncs stay clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant