Now.js Framework Documentation

Now.js Framework Documentation

TaskMonitorComponent

EN 04 Sep 2026 01:40

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:

  1. Idle backoff. While nothing is running the interval drops to
    idleInterval, or stops completely at 0.
  2. 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.
  3. 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