Skip to main content

Job-Log Card

A job-log card starts a long-running job and shows its progress as it runs. It has a small form and a log pane. Submitting the form posts to your endpoint, which starts an AsyncJobsMixin job and returns its AsyncJobRef. The card then streams the job's progress lines and shows the outcome when the job ends. The script is juneau-joblog.js, and <@page> loads it for you.

<@card type="job-log" id="rebuild" title="Rebuild index">
{ submit: { endpoint: '/ops/jobs/rebuild', submitLabel: 'Rebuild',
fields: [ { name: 'shards', label: 'Shards', type: 'number', default: 4 } ] },
urlParam: 'job' }
</@card>

From Java there is no typed builder; use CardSpec:

page.card(CardSpec.of("job-log", "rebuild").title("Rebuild index")
.body("submit", Map.of("endpoint", "/ops/jobs/rebuild", "submitLabel", "Rebuild"))
.body("urlParam", "job"));

The endpoint​

The submit endpoint receives the field values as JSON, plus an idempotencyKey that the card mints fresh for every submit, and returns AsyncJobRef.of(job). Because the card owns that key, a form field named idempotencyKey is rejected when the page renders. Report progress with job.progress("..."); each call becomes one line in the log pane, shown as text. The endpoint may also answer synchronously with an ActionResult instead of a job ref; the card then shows the outcome without a stream. The job's final ActionResult sets the banner:

OutcomeBanner
successDone.
failureThe job failed.
refusalThe job was refused.
cancelledCancelled.
cancelled-after-effectCancelled after the job had already made changes.
unknownThe job ended with an unknown outcome.

The result's message, if any, follows the banner text. A success banner is styled as ok and every other outcome as an error.

Action envelope​

By default (submit.envelope: 'flat') the field values are top-level keys of the body. With submit.envelope: 'action' the card sends the action-dialog shape instead, which needs submit.action:

{ "action": "rebuild", "targetId": "<key>", "idempotencyKey": "<key>", "fields": { "shards": 4 } }

On the page card targetId is the key itself, the self-targeted form an action dialog sends for a modal that is not bound to a row, so an endpoint that already serves such a dialog can serve the card unchanged. Because the fields are nested, no field name is reserved in this mode. A number field left empty is sent as null.

Hidden fields​

A field with type: 'hidden' renders an <input type="hidden">: no label, not focusable, and never reported as a missing required field. On the page card it sends its default. In a row-detail region it can take its value from the row instead (see In a row-detail region).

Output​

output writes a value from the terminal result into the card, once, when the job ends:

output: { target: { field: 'answer' }, from: 'row.answer', mode: 'replace', when: 'success' }
KeyMeaning
target.fieldThe element stamped data-juneau-field="<name>". Every input the card builds carries this stamp, so a field can receive the result.
target.selectorA CSS selector, first match. Exactly one of field and selector is set.
frommessage (default), the result's message; or row.<key>, an own property of the result's row.
modereplace (default), or append, which adds a new line after any existing text. An absent value writes nothing.
whensuccess (default) writes only on a successful outcome; any writes on every outcome.

The target is looked up inside the card only (in a region, inside the region), so a result can't reach the rest of the page. An input or textarea gets its value set; any other element gets its text set. The value is never written as markup. A target that isn't found, or a selector that isn't valid, logs E-JS-112 and writes nothing.

Body keys​

KeyMeaning
submit.endpointRequired. A same-origin path (no scheme, no // prefix, no .. segment).
submit.methodPOST (default) or PUT.
submit.envelopeflat (default) or action (see Action envelope).
submit.actionThe action id, matching [A-Za-z][A-Za-z0-9_-]{0,63}. Required with envelope: 'action'.
submit.fieldsForm fields: { name, label, type, placeholder, required, default, source }. name is required and type is text, textarea, number or hidden; any other type, or any other key, fails the page. In the flat envelope a name of idempotencyKey is reserved. source needs a row, so it is only allowed in a row-detail region. A missing required field shows an error banner and focuses the field; nothing is sent.
submit.submitLabelThe button text (default "Run").
heightThe log pane height in pixels, at least 80 (default 240).
urlParamMirror the running job id into this URL query parameter (a name matching [A-Za-z][A-Za-z0-9_-]*).
clearOnSubmitClear the previous run's log on a new submit: true (default) or false.
outputWrite a value from the result into the card (see Output).

Cancel and reload​

While a job runs, the button becomes Cancel, which posts to the job's cancelUrl; it is disabled when the AsyncJobRef has no cancelUrl. Whichever ends the job first, the cancel response or the stream's result, sets the banner. A cancel that fails shows its message in an error banner and leaves the job running.

The card remembers the running job in the browser's local storage. A fresh attach to a job stream is live-only, so the card always connects with ?lastEventId=0 added to the streamUrl and gets the progress so far, both straight after a submit and after a reload. Every progress event carries an SSE id; when the browser reconnects a dropped stream by itself it sends Last-Event-ID, and the server resumes after the last line shown. The card shows every progress line it receives and does no client-side de-duplication, so a server that repeats a line shows a visible repeat rather than hiding it. With urlParam, it reconnects only when the URL names the same job. A job the server no longer knows (or any stream that closes before delivering a single event, which includes a 429 or 5xx on attach) shows "Job not found; it may have finished or the server restarted." in the log pane and returns the form to its idle state, and the stored job and URL parameter are cleared. If the stream drops for good after it had delivered events, a warning banner says the job may still be running and to reload to reattach. A browser without EventSource shows a warning that live progress is unavailable. New run forgets the finished job and shows the form again.

In a row-detail region​

The same card can live inside a table row's detail panel, where it runs a job for that row. Declare it as the detail's region with populate: 'job-log'; the region's config takes the card's body keys plus submit.keyUrl and an optional detail grid:

<@card type="datatables" id="species">{
dataMode: 'client', dataUrl: '/console/ops/jobs/species',
columns: [ { key: 'species', label: 'Species' }, { key: 'available', label: 'Available' } ],
detail: {
endpoint: '/console/ops/jobs/species/{id}',
region: {
id: 'restock-species', populate: 'job-log',
config: {
submit: {
endpoint: '/console/ops/jobs/species/{id}/restock', action: 'restock-species',
keyUrl: '/console/ops/jobs/species/{id}/key', submitLabel: 'Restock',
fields: [ { name: 'count', label: 'How many', type: 'number', default: 1, required: true },
{ name: 'species', type: 'hidden', source: 'row.species' } ]
},
detail: { renderer: 'fieldGrid', fields: [ { data: 'available', label: 'Available' } ] },
output: { target: { field: 'available' }, from: 'row.available' }
}
}
}
}</@card>

From Java, build the same region with JobLogRegion and hand it to the row detail:

RowDetail.create("/console/ops/jobs/species/{id}")
.region(JobLogRegion.create("restock-species")
.submit(JobLogRegion.Submit.create("/console/ops/jobs/species/{id}/restock", "restock-species",
"/console/ops/jobs/species/{id}/key")
.submitLabel("Restock")
.field(JobLogRegion.Field.of("count").label("How many").type("number").defaultValue(1).required(true))
.field(JobLogRegion.Field.of("species").type("hidden").source("row.species")))
.detail(JobLogRegion.Detail.of("fieldGrid", RegionDef.Field.of("available").label("Available")))
.output(JobLogRegion.Output.field("available").from("row.available"))
.toRegion());

In a region:

  • The envelope is always action, and targetId is the row's own id, read from the panel's row stamp. It never comes from a field or from row data. envelope may be omitted; any value other than action fails the page.
  • The key comes from the server. When the panel opens, the card posts to submit.keyUrl (with {id} replaced) and keeps the submit button disabled until a key for this row and action arrives. A refused or mismatched key shows E-JS-109 and a Retry button. A key is reused only until the server answers; the next run fetches a fresh one.
  • {id} is the only token in submit.endpoint and submit.keyUrl, and both are same-origin path templates. urlParam is rejected.
  • source: 'row.<field>' fills a field from the row's data: an own property of the parent row only, never another row, the page or the DOM. The name matches [A-Za-z_][A-Za-z0-9_]{0,63}, with no paths. A hidden field binds at submit, from the row as of the click; a visible field is prefilled when the panel renders and the user's edit wins. Only strings, finite numbers, booleans and null are allowed: anything else falls back to the field's default, and with no default the submit is refused with E-JS-110 and nothing is sent.
  • detail paints a field grid above the form with the named JuneauViews.helpers renderer and the row's detail payload. Its data-juneau-field slots are what output.target.field addresses, so a result can update the panel's own values.
  • One job per row. While the row's job runs, the row is marked and its row actions are disabled; a row action activated anyway (from a menu, a pill or a detail button) is refused with row-job-running and sends nothing. Collapsing the panel leaves the job running; expanding it again re-attaches and replays the log from the start. The re-attached view has no submit until the job ends. While it runs, it shows Cancel when the job has a cancelUrl, and a cancel from either view goes through JuneauViews.jobs.cancel. The outcome still comes from the job's stream. A job can be watched from one re-attached view at a time; a second collapse and re-expand during the same job may not re-attach. A page reload does not re-attach.
  • The table's poll waits. While the open panel's job runs, the table's timer reload is held and the staleness pill shows Paused, so the panel isn't redrawn under the log.

The card registers its job-log populator whatever the script order. An app that registers its own job-log populator first keeps it, and one registered later replaces the card's. A page that declares a job-log region without juneau-views.js and juneau-regions.js logs E-JS-111. A datatables card with a job-log region pulls the joblog asset pack on its own; you don't add requires.

window.JuneauJobLog.mount(el, config, {signal}) mounts the same row-bound card programmatically into any element inside a view table row or its detail panel, from a region config. It returns {destroy, cancel, phase}; aborting signal removes the view but never cancels the job. It throws E-JS-111 when JuneauViews.jobs is missing.

The row-job runtime, helpers.jobButton, and the server side (JobKeyLedger, RowJobSubmit) are covered in Row Jobs.

Security​

The streamUrl and cancelUrl of the returned AsyncJobRef must resolve to the page's own origin, because the cancel request carries the console's CSRF token. A ref from another origin is refused with E-JS-77. The same check applies to a job ref read back from local storage after a reload, except that a bad stored ref is silently ignored (no E-JS-77) and the card starts idle. submit.endpoint is checked at render by the same same-origin-path rule as ChartDef hrefs, and again in the browser before each submit, because that request carries the CSRF token too: an endpoint that resolves to another origin, contains a backslash or starts with a space is refused with E-JS-81 and nothing is sent.

In a row-detail region the server's key binding is what makes the request row-bound. The key endpoint issues a key bound to the action, the row id and the caller, and the submit endpoint verifies all three; a key issued for one row is refused for any other with the same 409 used for an unknown or expired key. Presets from source and hidden fields are ordinary request data that the user can edit, so the server must never decide which row to act on, or whether the caller may act, from them. Use the verified targetId. The key endpoint must apply the same authorization as the action it keys.

Accessibility​

The log pane is role="log" with aria-live="polite" and takes keyboard focus, so a screen reader reads new lines as they arrive. Errors, including a rejected submit, a missing required field and a failed job, appear in a banner with role="alert", so they are announced at once, and again if the same error comes back. The ok and warning banners (a successful job, an interrupted stream, a browser without EventSource) have no role and are announced politely through the console's status region instead. When the Run/Cancel button changes or disappears, keyboard focus moves to the control that replaces it, or to the log pane, rather than falling back to the page.

Errors​

CodeWhen
E-JS-77The submit response is not a valid AsyncJobRef: jobId or streamUrl is missing, or streamUrl or cancelUrl is not same-origin. Shown in an alert banner above the form, which stays usable.
E-JS-78The stream's result event could not be parsed, or has no outcome. Shown in the alert banner.
E-JS-81submit.endpoint is not a same-origin path. Shown in the alert banner; nothing is sent.
E-JS-109Region mode: the key endpoint refused, or answered with a body whose contractVersion, action, targetId or key doesn't match this row and action. Shown in the alert banner with a Retry button; submit stays disabled.
E-JS-110A source: 'row.<field>' preset didn't resolve to a string, finite number, boolean or null, and the field has no default; or a hidden field has neither a source nor a default (the message then names only the missing default, since a page card has no row). Nothing is sent.
E-JS-111A job-log region, JuneauJobLog.mount or helpers.jobButton was used without window.JuneauViews.regions or window.JuneauViews.jobs.
E-JS-112output.target was not found inside the card (or region), or output.target.selector is not a valid selector. Nothing is written.
E-JS-113A JuneauViews.jobs hook threw. It is logged and the job's stream carries on.

Java checks the card body when the page renders: a missing submit.endpoint, an unknown key in the body, in submit or in a field, a cross-origin submit.endpoint, a submit.method other than POST or PUT, a field without a name or with a type other than text, textarea, number or hidden, a height below 80, a malformed urlParam, a clearOnSubmit that isn't a boolean, a field named idempotencyKey in the flat envelope, an envelope: 'action' without submit.action, a source on the page card, a malformed output, and a src= or template= on the card all fail the page. A job-log region's config is checked by the same rules (the shared JobLogChecks), plus a required keyUrl, {id}-only templates and no urlParam.

A rejected submit (for example a job limit) shows the server's message above the form.