Now.js Framework Documentation
TaskMonitorComponent
TaskMonitorComponent
Overview
TaskMonitorComponent watches long-running server tasks and reports when they end.
It exists for work that outlives the request that asked for it. Any server
operation measured in minutes — archiving files, importing data, generating a
report, issuing a certificate — cannot finish inside the request that starts it,
so the server starts it in the background and answers 202 Accepted straight
away.
From that point the page that pressed the button is no longer the page that sees
the result. The tab gets closed, another person opens the same screen, or the
browser is left alone for an hour. The answer cannot live in a browser tab, so
this component asks the server instead, on an interval, and renders what it says.
When to use:
- A button starts work the server finishes later
- A screen must show "still running" across reloads and across users
- Something on the page should react when a task ends
Why use it:
- ✅ No hard-coded API shape — every field is mapped through data attributes
- ✅ Polls only while something is running, then backs off to an idle interval
- ✅ Pauses entirely while the browser tab is hidden
- ✅ Backs off on errors instead of hammering a struggling server
- ✅ Fires a DOM event as each task finishes, so anything on the page can react
- ✅ Escapes every value it renders
Basic Usage
HTML Declarative
<div data-component="task-monitor"
data-endpoint="/api/v2/jobs"
data-items="data.active"
data-label-field="label"
data-status-field="status"></div>That markup expects a response shaped like this:
{
"ok": true,
"data": {
"active": [
{"id": 12, "label": "Backup example.com", "status": "running", "message": ""}
]
}
}Nothing about that shape is required. data-items is a dot path to wherever the
array lives ('' means the body itself is the array), and the four *-field
options name the keys inside one task.
JavaScript
const monitor = TaskMonitorComponent.create(document.querySelector('#tasks'), {
endpoint: '/api/tasks',
interval: 2000,
onFinish: (task) => console.log('done:', task.label)
});
// After starting a task, ask again immediately instead of waiting out the interval
TaskMonitorComponent.refresh(monitor);Options
Every option can be set as a data-* attribute in kebab-case: idleInterval
becomes data-idle-interval.
| Option | Default | Meaning |
|---|---|---|
endpoint |
'' |
Required. URL to poll |
items |
'data.active' |
Dot path to the array of tasks · '' = the body itself |
idField |
'id' |
Key that identifies one task across polls |
labelField |
'label' |
Key holding the name to display |
statusField |
'status' |
Key holding the status value |
messageField |
'message' |
Key holding the result text |
progressField |
'progress' |
Key holding 0–100 · absent = no progress bar |
runningValues |
'queued,running,pending,active' |
Status values meaning "still working" |
failedValues |
'failed,error,lost,cancelled' |
Status values meaning it ended badly |
interval |
3000 |
Poll interval while something is running (ms) |
idleInterval |
30000 |
Poll interval while nothing is · 0 = stop polling |
maxErrors |
0 |
Give up after this many consecutive failures · 0 = never |
maxInterval |
120000 |
Ceiling for the error backoff (ms) |
autoRender |
true |
false = the callbacks do the rendering |
emptyText |
'' |
Shown when nothing is running · empty = render nothing |
hideWhenIdle |
true |
Set hidden on the element while nothing is running |
notify |
true |
Show a toast per finished task, when NotificationManager is loaded |
Callbacks
| Callback | Signature | Called |
|---|---|---|
onInit |
(instance) |
After the first successful poll |
onUpdate |
(tasks, instance) |
After every successful poll |
onFinish |
(task, instance) |
Once per task, as it stops running |
onError |
(error, instance) |
On a failed poll |
onRender |
(tasks, instance) |
Instead of the built-in rendering |
onDestroy |
(instance) |
When the instance is destroyed |
Methods
| Method | Description |
|---|---|
init(options?) |
Mount every matching element · called automatically on DOM ready |
create(element, options?) |
Create one monitor · returns the instance or null |
refresh(instanceOrElement) |
Poll now, whatever the schedule said |
destroy(instanceOrElement) |
Stop polling and forget the instance |
Events
task-monitor:finished
Fires once per task, as it stops running. Bubbles, so a listener anywhere up the
tree receives it.
document.addEventListener('task-monitor:finished', (event) => {
const {task, label, message, failed} = event.detail;
if (!failed) TableManager.refresh('backups');
});A DOM event rather than only a callback, so a page can react without holding a
reference to the instance — refreshing a table, closing a dialog, or re-enabling
a button somewhere else on the screen entirely.
How "finished" is decided
The component remembers which task ids were running on the previous poll. A task
that was in that set and is not running now has finished — whether the server
now reports it as finished or has dropped it from the list altogether. Both mean
the same thing to whoever is watching, and without remembering, a task that
simply disappears is indistinguishable from one that was never there.
runningValues is a closed set on purpose. A status nobody anticipated — a new
server version adding cancelled, or a typo — counts as finished and stops the
poll, rather than leaving the page polling forever for something that already
ended.
Polling behaviour
Three separate things keep this from becoming a load problem:
- Idle backoff. While nothing is running the interval drops to
idleInterval, or stops completely at0. - Hidden tabs stop. A background tab polls nothing at all; returning to it
polls immediately, so the screen is never stale for longer than one request. - Error backoff. Each consecutive failure doubles the wait up to
maxInterval. Without it, a monitor left open against a restarting server
would send a request every few seconds for as long as the tab stays open —
the shape of a self-inflicted denial of service against the very machine that
is already struggling.
Styling
The component renders its own classes and ships Now/css/task-monitor.css:
<ul class="task-monitor-list">
<li class="task-monitor-item is-running">
<span class="task-monitor-status">running</span>
<span class="task-monitor-label">Backup example.com</span>
<progress class="task-monitor-progress" max="100" value="40"></progress>
<span class="task-monitor-message">…</span>
</li>
</ul>State is is-running, is-done or is-failed. Those are the component's own
modifiers, never classes borrowed from an application's stylesheet — a framework
component that renders pill-danger looks correct only in the one project that
happens to define it.
For a different layout entirely, use onRender and ignore all of it.
Security
Everything rendered comes from the server, and a task label is very often a
filename or a domain somebody else chose — exactly the values a customer can
control. Every string is escaped before it reaches innerHTML; rendered raw, one
of them is a stored XSS in an administrator's own screen.
onRender replaces the built-in rendering entirely, so a custom renderer is
responsible for its own escaping.
Dependencies
| Dependency | Required | Used for |
|---|---|---|
CoreObserver |
optional | Mounting elements added later · falls back to its own MutationObserver |
window.http |
optional | Requests, with the session and CSRF handling the app already uses · falls back to fetch |
NotificationManager |
optional | The toast shown as a task finishes |
Tests
tests/task-monitor.mjs runs the real built bundle in a real browser against a
real HTTP server — 18 checks covering mounting, the finished event, unknown
statuses, escaping, error backoff and timer cleanup.
cd tests && node task-monitor.mjs