Standalone web app for eDesk ticket statistics: cross-tabs of received and handled (closed) ticket volumes, groupable by day / week / month, by service and by user, with Excel export. Two more tabs add an inbound/outbound message volume view and a flow graph of how tickets move between services.
created_at within the period.status === "Closed". Close date and service:
last_updated_at with the current tags (the API
exposes no dedicated close date). The fallback applies to tickets closed before history
started.channel_id), via
config/channel-services.json (mirrors the channel's "Associated tags" in eDesk, which
the API does not expose). One ticket = one channel = one service; unmapped channel →
"Canal inconnu"; ignored channel → ticket excluded.owner_user_id resolved to a name; null → "Non assigné"..xlsx) via exceljs — one sheet per displayed table.cache/history.sqlite), fed automatically
(initial backfill + hourly node-cron poller). No JSON cache, no button.Image published on Docker Hub: ppcm/edesk-stats (multi-arch amd64/arm64).
Minimal docker-compose.yml:
services:
edesk-stats:
image: ppcm/edesk-stats:latest
ports:
- "3000:3000"
environment:
EDESK_API_TOKEN: "your_read_only_token"
BACKFILL_START_DATE: "2026-01-01"
SNAPSHOT_ENABLED: "true"
SNAPSHOT_CRON: "0 * * * *"
volumes:
# host folder ./cache <-> /app/cache in the container (see Persistence)
- ./cache:/app/cache
restart: unless-stopped
docker compose up -d # http://localhost:3000
On the first start (empty volume), the initial backfill automatically loads the tickets
created since BACKFILL_START_DATE, then the poller keeps the database up to date.
The inbound/outbound message metric (the "Messages" tab) is built forward by the poller
(bounded by MESSAGES_FETCH_BUDGET). To populate recent message history in one pass, run the
bounded, resumable backfill (re-run to continue if it stops at the budget):
node scripts/backfill-messages.js --from 2026-06-01 [--to 2026-07-01] [--budget 100000]
Message backfill is slow and rate-limited (one API call per message, no bulk endpoint). Read Message collection & rate limiting before running a large window — set
--toto today and keep concurrency low.
| Variable | Required | Default | Purpose |
|---|---|---|---|
EDESK_API_TOKEN | yes | — | Read-only eDesk API token. |
BACKFILL_START_DATE | yes | — | Start date of the initial load — format YYYY-MM-DD (tickets created since this date). |
SNAPSHOT_ENABLED | no | false | true to enable the node-cron poller (recommended). |
SNAPSHOT_CRON | no | 0 * * * * | Poller frequency (cron). Lower it for higher history fidelity. |
SNAPSHOT_LOOKBACK_DAYS | no | 2 | Minimum re-scan overlap (days). The real window starts at the earlier of this floor and the persisted high-water mark — see Message collection & rate limiting. |
SNAPSHOT_DB_PATH | no | cache/history.sqlite | SQLite database location. |
MESSAGES_FETCH_BUDGET | no | 2000 | Max messages fetched per poller run for the inbound/outbound metric; 0 disables. |
MESSAGES_FETCH_CONCURRENCY | no | 2 | Parallel message fetches. Keep low — the eDesk API rate-limits bursts hard (see below). |
API_MAX_PER_MINUTE | no | 60 | Proactive cap on outgoing requests (rolling 60s window) to stay under eDesk's ~60 calls/min limit; 0 disables. |
API_MAX_RETRIES | no | 12 | Retries per request on 429 / transient network errors before giving up. |
API_MAX_BACKOFF_MS | no | 30000 | Cap for the (jittered, exponential) retry backoff. |
ACCOUNT_TIMEZONE | no | Europe/Paris | Timezone used to convert message Unix timestamps to local dates. |
HANDLED_DATE_FIELD | no | last_updated_at | Date field for "handled" (fallback, outside history). |
PORT | no | 3000 | HTTP server port. |
EDESK_BASE_URL | no | https://api.edesk.com/v1 | API base URL. |
The entry channel → service mapping lives in
config/channel-services.json(baked into the image). To adjust it without a rebuild, mount it as a volume:-v ./config/channel-services.json:/app/config/channel-services.json:ro.
Each poller run re-scans the tickets updated (last_updated_at) in a window, re-snapshots
them and caches any new messages. The window does not use a fixed lookback; it starts at a
persisted high-water mark (poll_watermark, stored in the DB meta table):
SNAPSHOT_LOOKBACK_DAYS is only a minimum overlap floor (safety for in-flight updates and
timezone edges). The effective window start is the earlier of that floor and the mark.The eDesk API has no bulk/list endpoint for messages: each message is fetched individually
via GET /v1/messages/{id}. It also enforces a strict rolling rate limit that returns
429 with no Retry-After header. Bursts of concurrent requests trip it within seconds and
can throttle the whole account for a while; the sustainable rate observed is roughly ~2 req/s.
Consequences:
API_MAX_PER_MINUTE (default 60,
rolling 60s window) so it stays under the limit instead of only reacting to 429s. Applied at
the single request choke point, it covers ticket paging, tag/user lookups and message fetches
alike. Set it to 0 to disable.MESSAGES_FETCH_CONCURRENCY low (default 2). With the per-minute cap in place,
higher concurrency does not speed things up — the limiter is the ceiling; concurrency only
helps saturate it when individual calls are slow (> 1s).API_MAX_RETRIES, API_MAX_BACKOFF_MS) instead of aborting.scripts/backfill-messages.js walks the window day by day and caches missing messages. It is
resumable (already-cached ids are skipped) and fault-tolerant (a day that fails is
skipped and retried on the next run):
MESSAGES_FETCH_CONCURRENCY=2 node scripts/backfill-messages.js \
--from 2026-06-01 --to 2026-07-01 --budget 100000
--to to today, not the last day you care about: a message's ticket may have been
updated later, and the backfill only sees a ticket on the days it was updated.Size MESSAGES_FETCH_BUDGET (per hourly run) above the peak hourly message inflow so the poller
keeps pace — the default 2000 suits a few-thousand-messages/day account. If a run risks
overlapping the next one, lower the budget or space out SNAPSHOT_CRON (e.g. 0 */2 * * *).
/app/cache.Content type
Image
Digest
sha256:b711c6cc3…
Size
98.6 MB
Last updated
1 day ago
docker pull ppcm/edesk-stats