REST API
Base URL: http://localhost:3000 (or whatever you bound the dashboard to).
All API calls (except the public ones below) require the tg_dl_session cookie. Hit POST /api/login first to get one.
Authorization model
The session cookie carries one of two roles:
admin— full access.guest— opt-in read-only viewer. A default-deny chokepoint allowlists only the read endpoints listed below; every mutation route returns403 {adminRequired:true}for guest sessions.
A few /api/auth/* routes are explicitly registered before the global auth middleware and enforce their own checks (login / setup / change-password / reset / guest-password). The public /share/<id> route also bypasses dashboard auth — it is gated by HMAC signature + DB row check instead.
Auth & setup
| Method | Path | Notes |
|---|---|---|
GET | /api/auth_check | Public. {configured, enabled, authenticated, role, setupRequired, guestEnabled}. |
POST | /api/auth/setup | Public, localhost-only. First-run password — {password}. |
POST | /api/login | Public. {password} → sets cookie, returns {success, role}. Rate-limited 10/15min/IP. Server tries the admin hash first, then the guest hash. |
POST | /api/logout | Revokes the current session. |
POST | /api/auth/change-password | {currentPassword, newPassword}. Admin only. Rejects collisions with the guest password. |
POST | /api/auth/reset/request | Public. Prints a 10-min reset token to stdout. |
POST | /api/auth/reset/confirm | Public. {token, newPassword} — resets the admin password and revokes every active session. |
POST | /api/auth/guest-password | Admin only. {password?, enabled?, clear?} — manage the guest password. |
Telegram accounts
| Method | Path | Notes |
|---|---|---|
GET | /api/accounts | Saved sessions. |
POST | /api/accounts/auth/begin | {label?} → {sessionId, state:'phone'}. |
POST | /api/accounts/auth/phone | {sessionId, phone} → {state:'code'\|'phone'\|'error', error?, code?, seconds?}. |
POST | /api/accounts/auth/code | {sessionId, code} → {state:'password'\|'done'\|'code'\|'error', accountId?, hint?}. |
POST | /api/accounts/auth/2fa | {sessionId, password} → {state:'done'\|'password'\|'error', accountId?}. |
POST | /api/accounts/auth/cancel | {sessionId}. |
GET | /api/accounts/auth/:sessionId | Status polling. The first poll that sees done loads the new account into the running engine. |
A wrong phone number / code / password keeps the step’s state and sets error plus code — the Telegram error name (PHONE_NUMBER_INVALID, PHONE_CODE_INVALID, PHONE_CODE_EXPIRED, PASSWORD_HASH_INVALID, …) or FLOOD_WAIT with seconds; submitting that step again retries. hint is the 2FA password hint. begin answers 503 {code:'NO_API_CREDS'} until telegram.apiId / apiHash are set. | DELETE | /api/accounts/:id | Removes the saved session. |
Monitor / engine
| Method | Path | Notes |
|---|---|---|
GET | /api/monitor/status | {state, queue, active, workers, accounts, stats, uptimeMs, hint}, plus core: {state, fix} only while tgdl-core (the app’s Go engine) can’t run — the dashboard shows it as a banner. Also broadcast over WS as monitor_status_push every 3 s when at least one client is connected. |
POST | /api/monitor/start | Loads AccountManager, starts realtime monitor in-process. |
POST | /api/monitor/stop | Cleans up watchers + the worker pool. |
Stats / dialogs / groups
| Method | Path | Notes |
|---|---|---|
GET | /api/stats | {totalFiles, totalSize, diskUsage, telegramConnected, peerStats:[{peerId, peerName, online, totalFiles, totalSize, totalSizeFormatted}], …}. Also broadcast over WS as stats_push every 30 s. peerStats is [] for non-cluster installs and for guest sessions. |
GET | /api/dialogs | Active + archived chats; DMs gated by config.allowDmDownloads. Each row has access (see Chat access below). Fetching the lists also syncs access for free: a chat back in an account’s list flips to ok; a configured chat listed as forbidden / migrated is recorded. |
GET | /api/chats/lookup?q= | Resolve what the dashboard’s Add box can’t find by name: @username, t.me/<name>, t.me/c/<id>, invite links (t.me/+…, joinchat/…) and message links. → {kind, chat?, invite?, message?}; chat has id, name, type, username, members, joined, inConfig, enabled, suspended, dmDisabled, access. An invite this account isn’t in returns an invite preview (title, members, url). 404 not_found / invite_invalid, 422 for t.me links that aren’t chats, 503 no_account. |
GET | /api/groups | Configured groups with photo URLs. Each own row has access (see Chat access below) and, when its auto-forward destination refused our posts, forwardAccess: {state, code, nextCheckAt}. |
PUT | /api/groups/:id | Update group config (filters, autoForward, topics, accounts, cluster routing — ownerPeerId / backupPeerId). Auto-spawns a first-add backfill when the group is newly enabled and has no rows yet. |
DELETE | /api/groups/:id/purge | Drop files + DB rows + config + photo. |
GET | /api/groups/:id/photo | Cached profile photo. |
POST | /api/groups/refresh-photos | Re-fetch profile photos for every configured group. |
POST | /api/groups/refresh-info | Re-resolve every monitored chat name from Telegram. Chats that can’t be reached are skipped (same for refresh-photos, resync-dialogs and /api/groups/:id/photo). |
Chat access
Whether a chat can still be used, one standard answer everywhere. A chat that no loaded account can read is paused — polling, the update handler, the downloader (queued files are dropped, no retries), backfill, avatar / name lookups, Stories and auto-forwarding skip it with a local check, no Telegram call — until a re-check or a dialogs sync sees it readable again. Its config entry is left as it is (enabled stays the operator’s choice); downloaded files are never touched and nothing is left or unsubscribed in Telegram.
access object (on /api/groups, /api/dialogs and /api/chats/lookup rows):
{ "state": "private", "code": "CHANNEL_PRIVATE", "detail": null, "migratedTo": null,
"firstSeenAt": 1790535647415, "checkedAt": 1790622047415, "nextCheckAt": 1790644432703,
"checks": 1, "accounts": [{ "id": "123", "state": "private", "code": "CHANNEL_PRIVATE", "at": 1790622047415 }] }
state:ok·left(no account is a member) ·banned(kicked / banned) ·private(private channel, access lost) ·deleted(deactivated / doesn’t exist) ·restricted(restricted by Telegram —detailhas Telegram’s text) ·migrated(a basic group upgraded to a supergroup —migratedTois the new id) ·unknown(couldn’t tell: flood wait, timeout, no account connected — never pauses anything). A chat with nothing against it is just{ "state": "ok" }.codeis Telegram’s error (CHANNEL_PRIVATE,CHANNEL_INVALID,USER_BANNED_IN_CHANNEL,CHANNEL_PUBLIC_GROUP_NA, …) or the entity shape (CHANNEL_FORBIDDEN,CHAT_MIGRATED, …).accounts: each account’s own answer. The chat is only paused when no account can read it; if one still can, it takes over (and is pinned) and the chat staysokwith the other account’s failure listed. Empty for guest sessions.- Re-checks: one chat per minute at most, after 1 h, 6 h, then daily (
nextCheckAt), for monitored chats only;migratedis permanent and isn’t re-checked. Adding an account makes every paused chat due. legacy: truemarks an entry an older version switched off (suspended/_resolveFailedAtin the config); those flags are cleared once the chat is reachable again.
| Method | Path | Notes |
|---|---|---|
GET | /api/chats/access | Configured chats that can’t be reached → {total, byState, items:[{id, name, type, enabled, access}]}. ?countOnly=1 → {total, byState}. |
POST | /api/chats/access/recheck | “Check again”. {id} → checked now (each account once, pinned first, stopping at the first that can read) → {id, state, access, accountId, inconclusive, results}; inconclusive: true means no account gave a definite answer (flood wait, timeout, none connected) and nothing changed. {ids:[…]} or {all:true} (every configured chat that can’t be reached) → {started, total}, run in the background one chat every 2 s; progress on WS chat_access_recheck_progress / chat_access_recheck_done, status at GET /api/chats/access/recheck/status. 409 ALREADY_RUNNING. |
POST | /api/chats/access/stop | {ids} → enabled:false for each → {stopped}. |
POST | /api/chats/access/remove | {ids} → removes the config entries only → {removed}. Downloaded files and their gallery rows stay. |
POST | /api/chats/:id/follow-migration | A migrated chat: adds the new supergroup with the old entry’s settings (media types, forwarding, topics, accounts, rescue, cluster routing) and switches the old one off → {added, group, previous}. 409 NOT_MIGRATED otherwise. |
All five are admin-only. POST /api/history for a chat that can’t be reached answers 409 {code:'CHAT_UNREACHABLE', access} before any Telegram call (auto-first and catch-up backfills are skipped); POST /api/stories/* does the same for a known chat, and POST /api/download/url reports code: 'CHAT_UNREACHABLE' per link. GET /api/maintenance/recovery/list includes paused chats with resolveFailedReason: "access:<state>:<code>" and access.
Downloads
| Method | Path | Notes |
|---|---|---|
GET | /api/downloads | Aggregate per group. |
GET | /api/downloads/all | Cross-group All-Media list, paginated. ?page=&limit=&type=. ?include=local\|peers\|all (admin-only) UNIONs peer_downloads into the result; ?peerId=<id> narrows to one peer. Each row carries peer_id ('self' or peer’s id) + peer_name. Default local is backward-compatible. |
GET | /api/downloads/:groupId | Paginated rows for one group. ?type=images\|videos\|documents\|audio. Same ?include= / ?peerId= federation params as /all. |
GET | /api/downloads/search | ?q=…&page=&limit=&groupId=. Optional type= (images / videos / documents / audio), pinned=1, pinnedFirst=1 (same as the gallery feeds) and order=newest (default: FTS relevance). File name / chat name prefix match, falling back to a substring match when that finds nothing. Same ?include= federation param. |
POST | /api/downloads/bulk-delete | {ids?, paths?}. Also purges thumbnail cache for every removed id. |
POST | /api/downloads/pin | {ids:[…], pinned} — pin / unpin many rows in one request (max 5000 ids, else 413). Returns the ids that exist. |
POST | /api/downloads/:id/pin | {pinned} — one row. |
DELETE | /api/file?path=… | Single file. |
DELETE | /api/purge/all | Factory reset. Body {"confirm": "DELETE ALL"} (exactly); without it 400 {code: "CONFIRM_REQUIRED"} and nothing is touched. Starts a job: {started: true}, progress on purge_all_progress / purge_all_done. |
Direct downloads
| Method | Path | Notes |
|---|---|---|
POST | /api/download/url | {url} or {urls:[…]} — t.me / tg:// URLs. Goes through the same registerDownload chokepoint (thumb + NSFW hooks fire). |
POST | /api/stories/user | {username} → list of active stories. |
POST | /api/stories/all | All visible stories grouped by peer. |
POST | /api/stories/download | {username, storyIds:[…]}. |
POST | /api/history | {groupId, limit?, offsetId?, mode?} → kicks off a backfill job. mode ∈ pull-older (default) / catch-up / rescan. Returns 409 with code:'ALREADY_RUNNING' if a job for the same group is in flight. |
GET | /api/history/jobs | {active:[…], recent:[…]}. Recent retention configurable via advanced.history.retentionDays. |
GET | /api/history/:jobId | One job. |
POST | /api/history/:jobId/cancel | Graceful cancel — partial results are kept. |
DELETE | /api/history/:jobId | Drop one finished entry. |
DELETE | /api/history | Clear every finished entry. |
Thumbnails
| Method | Path | Notes |
|---|---|---|
GET | /api/thumbs/:id | ?w=120\|200\|240\|320\|480 — server-generated WebP. Image source → sharp; video source → ffmpeg first-frame. Cache-Control: public, max-age=86400, immutable. Allowed for guest sessions. |
GET | /api/cluster/peer-thumbs/:remoteId | HMAC-only peer-to-peer thumb handler: the WebP bytes (image/webp, ?w= like /api/thumbs/:id). Sibling of /api/thumbs/:id for federation. |
GET | /api/cluster/thumbs/:peerId/:remoteId | Cookie-authed browser proxy that signs a request to peer’s peer-thumbs and streams the response. Returns a 1×1 placeholder PNG with Cache-Control: public, max-age=60 when the peer is offline or answers something that isn’t an image (older versions sent a JSON object here). |
Share links
| Method | Path | Notes |
|---|---|---|
POST | /api/share/links | Admin only. {downloadId, ttlSeconds?, label?} → {url, expiresAt, id}. ttlSeconds: 0 = “never expires” sentinel. |
GET | /api/share/links | Admin only. ?downloadId= filters to one file (Share sheet); no filter = all (Maintenance sheet). |
DELETE | /api/share/links/:id | Admin only. Idempotent revoke. |
GET | /share/:linkId | Public, gated by HMAC + DB row. ?exp=&sig= → streams the file via the same safeResolveDownload path that /files/* uses (Range-request friendly). 401 on bad/expired/revoked. |
Maintenance
| Method | Path | Notes |
|---|---|---|
POST | /api/maintenance/files/verify | Re-stat every cataloged download; prune rows whose file is missing on disk. 503 TGDL_CORE_UNAVAILABLE (with the fix) while tgdl-core can’t run. |
GET | /api/maintenance/files/verify/status | JobTracker snapshot — {running, stage, progress, result}. |
GET | /api/maintenance/files/verify/stats | {lastRun: {finishedAt, removed, scanned}} — survives restart. |
POST | /api/maintenance/reindex | Walk data/downloads/ and INSERT OR IGNORE rows for files the catalog doesn’t have yet. 503 TGDL_CORE_UNAVAILABLE while tgdl-core can’t run. |
GET | /api/maintenance/reindex/status | JobTracker snapshot. |
GET | /api/maintenance/reindex/stats | {lastRun: {finishedAt, added, scanned}}. |
POST | /api/maintenance/resync-dialogs | Re-resolve every group’s name + profile photo. |
POST | /api/maintenance/restart-monitor | Stop + start the in-process monitor. |
POST | /api/maintenance/db/integrity | PRAGMA integrity_check. |
POST | /api/maintenance/db/vacuum | VACUUM. |
POST | /api/maintenance/dedup/scan | SHA-256 catch-up + groups duplicate sets. Single in-flight guard; broadcasts dedup_progress over WS. 503 TGDL_CORE_UNAVAILABLE while tgdl-core can’t run. |
GET | /api/maintenance/dedup/status | JobTracker snapshot — {running, stage, processed, total, result}. |
GET | /api/maintenance/dedup/stats | {totalFiles, hashed, missing, lastScan: {finishedAt, scanned, hashed, duplicateSets, extraCopies, reclaimableBytes}}. Survives restart. |
POST | /api/maintenance/dedup/delete | {ids:[…]} — delete from disk + DB + thumbs cache. |
POST | /api/maintenance/thumbs/build-all | Generate default-width thumbs for every row that doesn’t have one. Broadcasts thumbs_progress. |
GET | /api/maintenance/thumbs/build/status | JobTracker snapshot. |
GET | /api/maintenance/thumbs/build/stats | {lastRun: {finishedAt, built, skipped, errored, scanned}}. |
POST | /api/maintenance/thumbs/rebuild | Wipe cache; re-generation happens lazily on next access. |
GET | /api/maintenance/thumbs/stats | {count, bytes, ffmpegAvailable, allowedWidths}. |
POST | /api/maintenance/faststart/scan | Sweep MP4s and rewrite ones whose moov atom isn’t at the head. Broadcasts faststart_progress. |
GET | /api/maintenance/faststart/status | JobTracker snapshot. |
GET | /api/maintenance/faststart/stats | {total, optimized, pending, missing, unknown, ext_skip, ffmpegAvailable, lastRun}. |
GET | /api/maintenance/nsfw/status | {enabled, running, scanned, total, candidates, keep, whitelisted, model, threshold, fileTypes}. |
POST | /api/maintenance/nsfw/scan | Start a background scan (returns 503 when feature is disabled, 409 when one is already running). |
POST | /api/maintenance/nsfw/scan/cancel | Abort the active scan; partial results kept. |
GET | /api/maintenance/nsfw/results | Paginated low-score rows (deletion candidates). ?page=&limit=. |
POST | /api/maintenance/nsfw/delete | {ids:[…]} — delete + purge thumbs. |
POST | /api/maintenance/nsfw/whitelist | {ids:[…]} — mark as confirmed-18+; future scans skip. |
POST | /api/maintenance/nsfw/sidecar-test | CORS proxy — test connection to an arbitrary NSFW sidecar URL. Body: {url}. Returns {ok, version, model, ready}. |
GET | /api/maintenance/logs | List data/logs/*.log with size + mtime. |
GET | /api/maintenance/logs/download | ?name=&lines= — tail of one logfile (50 MB cap). |
GET | /api/maintenance/config/raw | Redacted runtime config (kv-backed). |
POST | /api/maintenance/session/export | Password-gated session-string export. |
POST | /api/maintenance/sessions/revoke-all | Sign out every dashboard session. |
Seekbar previews (v2.17)
Opt-in feature — generates WebP sprite-sheet timeline thumbnails for video hover previews. Off by default; flip config.advanced.seekbar.enabled (Maintenance → Seekbar previews) to turn on. Backed by a standalone Go sidecar (seekbar-service/) that the dashboard auto-spawns on first use.
| Method | Path | Notes |
|---|---|---|
GET | /api/seekbar/sprite/:id | Serves the WebP (or JPEG fallback) sprite-sheet for the given downloads.id. Allowed for guest sessions. Cache-Control: public, max-age=86400, immutable. 404 when the sprite is still pregenerating or the feature is disabled. |
GET | /api/seekbar/meta/:id | Returns the JSON sidecar ({ cols, rows, frames, tile_w, tile_h, interval_sec, duration_sec, format, … }). Guest-readable. Same 404 semantics. |
POST | /api/maintenance/seekbar/build-all | Admin only. Starts the JobTracker backfill — paginates downloads WHERE file_type='video' AND id NOT IN (SELECT download_id FROM seekbar_sprites). Broadcasts seekbar_progress. |
POST | /api/maintenance/seekbar/build/cancel | Cancel the in-flight scan. |
GET | /api/maintenance/seekbar/build/status | JobTracker snapshot. |
GET | /api/maintenance/seekbar/build/stats | {lastRun: {finishedAt, processed, generated, skipped, errored, durationMs}}. Survives restart. |
POST | /api/maintenance/seekbar/rebuild | Wipe every sprite + the table; broadcasts seekbar_rebuild_progress. |
GET | /api/maintenance/seekbar/rebuild/status | JobTracker snapshot. |
POST | /api/maintenance/seekbar/regen/:id | Force-regenerate one row regardless of overwrite policy. |
GET | /api/maintenance/seekbar/stats | {count, bytes, ffmpegAvailable, lastRun}. |
GET | /api/maintenance/seekbar/list | Cursor-paginated row list. ?beforeId=&limit=. |
GET | /api/maintenance/seekbar/health | {sidecar:{ok, url, mode, version, pid?}, ffmpegAvailable} — drives the page’s status pill. |
GET | /api/maintenance/seekbar/hwaccel-probe | Proxies the sidecar’s hardware probe — {candidates[], available[], recommended}. |
POST | /api/maintenance/seekbar/sidecar-test | CORS proxy — test connection to an arbitrary seekbar sidecar URL. Body: {url, token?}. Returns {ok, version}. |
POST | /api/maintenance/seekbar/sidecar/restart | Tear down + respawn the Go sidecar. Use after changing hwaccel / concurrency / port range. Broadcasts seekbar_sidecar_status. |
AI / Face clustering (v2.16+)
Opt-in face detection + clustering, backed by the Python sidecar in faces-service/. Off by default; flip config.advanced.ai.enabled + config.advanced.ai.faceClustering. All endpoints are admin-only. See docs/AI.md for the deep dive.
| Method | Path | Notes |
|---|---|---|
GET | /api/ai/status | {enabled, faceClustering, sidecar:{ok, url, mode, version, providers_resolved, det_size, …}, scan, peopleCount, facesCount}. |
POST | /api/ai/scan/start | {feature:'faces'} — kicks off Phase A (detect+embed) and Phase B (DBSCAN). Auto-flips enabled=true so a fresh install doesn’t need a separate save round-trip. |
POST | /api/ai/scan/cancel | Cancels the active scan; partial detections are kept. |
GET | /api/ai/scan/status?feature=faces | JobTracker snapshot for the re-mounted page. |
GET | /api/ai/faces/provider-probe | Sidecar provider probe — {candidates[], available[], details[], recommended, current}. |
POST | /api/ai/faces/health-test | CORS proxy — test connection to an arbitrary faces sidecar URL. Body: {url}. Returns {ok, version, model, ready, providers}. |
POST | /api/ai/faces/restart | Restart the faces sidecar (after switching detector model / providers / det_size). Broadcasts ai_faces_status. |
POST | /api/ai/faces/install-deps | Stream python -m tgdl_faces.install over ai_faces_install_progress / ai_faces_install_done. Accepts {force?:'cpu'\|'gpu'\|'directml'\|'openvino', dryRun?:bool, noUninstall?:bool}. |
POST | /api/ai/faces/recluster | Re-run DBSCAN over the existing faces table without re-detecting (cheap; preserves labels via centroid match). 503 TGDL_CORE_UNAVAILABLE while tgdl-core can’t run. |
POST | /api/ai/faces/reindex | Confirm-sheet gated — wipes every detection + cluster and re-scans every photo. Use after switching detector model. Broadcasts ai_faces_reindexed. |
POST | /api/ai/preload-model/:name | Trigger background download of a face detection model. Proxies to sidecar POST /preload/:name. Returns {model, status}. status ∈ not_downloaded, downloading, ready, error:…. |
GET | /api/ai/preload-model/:name/status | Check model download status. Returns {model, status}. |
GET | /api/ai/people | Cluster list with cover-face + face count + video_face_count per person. ?page=&limit=. |
GET | /api/ai/people/:id/photos | Paginated photos in this cluster. |
PATCH | /api/ai/people/:id | {label} — rename. |
DELETE | /api/ai/people/:id | Drop cluster; faces become unassigned. |
POST | /api/ai/people/:id/merge | {otherId} — fold one cluster into another. |
POST | /api/ai/people/:id/split | {faceIds, newLabel?} — create a new cluster from selected faces. |
POST | /api/ai/faces/:id/reassign | {personId} — move a single face to another cluster. |
GET | /api/ai/faces/by-download/:id | Face boxes for the gallery viewer overlay. |
GET | /api/ai/group-by-person | Maintenance grid grouped by cluster — drives the People tab. |
Faces sidecar (direct)
The dashboard proxies these via /api/ai/preload-model/… above, but the sidecar also exposes them on its own port (default :7555).
| Method | Path | Notes |
|---|---|---|
POST | /preload/{model_name} | Download model files without switching the active model. Allowed models: buffalo_l, antelopev2, buffalo_m, buffalo_s, buffalo_sc. Returns {model, status}. |
GET | /preload/{model_name}/status | Check model download status. Returns {model, status}. status ∈ not_downloaded, downloading, ready, error:…. |
Auto-update
| Method | Path | Notes |
|---|---|---|
GET | /api/update/status | Any signed-in session (guests too — the status bar’s update sheet reads it); 401 without one. Capability probe — {available, inDocker, watchtowerConfigured, watchtowerUrl, overlayStallMs}. |
POST | /api/update | Admin only. Runs a 5-step pipeline: ping watchtower (5 s HEAD) → live-DB PRAGMA quick_check → snapshot to data/backups/db-pre-update-<UTC>.sqlite → verify the snapshot is openable + clean (bad files are deleted) → POST watchtower’s /v1/update. Returns 200 {started:true} on success or 4xx/5xx with a structured code: AUTO_UPDATE_UNAVAILABLE, WATCHTOWER_UNREACHABLE, DB_CORRUPT, BACKUP_FAILED, BACKUP_VERIFY_FAILED, TRIGGER_FAILED, or ALREADY_RUNNING. |
GET | /api/update/history | Admin only. Last N (default 25, max 200) update attempts from the update_history table — {from_version, to_version, started_at, finished_at, status, error_code, error_msg, backup_path, backup_bytes}. status is triggered (in-flight, not yet finalised), success (new container booted on a different version), failed (pre-flight or trigger threw), or stalled (watchtower acked but the swap never landed within 10 min). |
GET | /api/auto-update/status | Admin only. Live JobTracker snapshot for the in-flight /api/update run (running flag, stage, durations, last error). |
Config & proxy
| Method | Path | Notes |
|---|---|---|
GET | /api/config | Admin only. Secrets are write-only: web.password / web.passwordHash are left out, and telegram.apiHash, web.shareSecret, web.guestPasswordHash, proxy.password, advanced.nsfw.apiToken, advanced.seekbar.apiToken and advanced.ai.faces.sidecarToken are replaced by a <name>Set boolean (apiHashSet, shareSecretSet, guestPasswordHashSet, passwordSet, apiTokenSet, sidecarTokenSet). |
POST | /api/config | Deep-merge updates; advanced.* namespaces are clamped per-field on save and re-applied at runtime via config_updated. A secret left out of the body keeps its saved value (send proxy.password: null to clear it); the <name>Set flags are ignored. |
POST | /api/proxy/test | {host, port} → 5-s TCP probe. |
File serving
| Method | Path | Notes |
|---|---|---|
GET | /files/<path> | Serves files under data/downloads/. Default Content-Disposition: attachment; pass ?inline=1 for inline media (used by the SPA viewer). Tolerates the legacy data/downloads/ prefix. ?peer=<id> (admin-only) routes through streamFromPeer() to fetch the file from a paired peer (proxy mode) or 302-redirects to a signed share URL (direct stream mode). Guest sessions are 403’d when ?peer is present. |
GET | /photos/<id>.jpg | Cached profile photos. |
GET | /share/<linkId> | Public share-link route — see Share links above. |
WebSocket
ws://<host>:3000 (or wss:// behind TLS). Authenticates via the same session cookie at the upgrade handshake; the role (admin / guest) is stamped on the socket for future per-event filtering.
| Event type | Payload |
|---|---|
monitor_state | {state, error?} |
monitor_status_push | Full /api/monitor/status snapshot every 3 s. |
download_progress | {payload: {key, groupId, fileName, progress, received, total, bps}} |
download_complete | {payload: {key, groupId, fileName, fileSize, deduped?}} |
download_start / download_error / queue_length / queue_changed / scale / rate_wait / flood_wait / forward_error / rescued / monitor_download / monitor_urls / monitor_error / monitor_started | Engine events, like the two above: each goes out under its own type with the event’s data in payload (queue_length: {length}, download_error: {job, error}, rate_wait / flood_wait: {seconds}). There is no monitor_event envelope (older docs listed one; it was never sent). |
stats_push | Full /api/stats snapshot every 30 s. |
file_deleted | {path, id?} |
bulk_delete | {unlinked, dbDeleted, ids?} |
group_purged | {groupId} |
purge_all | {} |
groups_refreshed | {updates} |
chat_access_changed | {ids} — chats whose access state changed (coalesced over 0.5 s); reload /api/groups. Forward-destination entries carry a dest: prefix. |
chat_access_recheck_progress / chat_access_recheck_done | Job-tracker snapshots of a bulk “Check again” (progress: {processed, total, reachable}, result: {total, reachable, results}). |
history_progress | {jobId, processed, downloaded, group, mode} |
history_done / history_cancelled / history_error | as above |
history_deleted / history_cleared | Cross-tab Recent-backfills sync. |
history_stalled | {pending, cap, stallSeconds} |
dedup_progress | {stage, processed, total, hashed, errored} |
thumbs_progress | {stage, processed, total, built, skipped, errored} |
nsfw_progress | {scanned, total, candidates, keep, running} |
nsfw_done | {scanned, candidates, keep, durationMs} |
nsfw_blocklist_deleted | {id, key} — a file auto-deleted by the NSFW hash blocklist. key is the job key (groupId:messageId:mediaType) for queue UI matching. |
nsfw_model_downloading | {percent} (first-run only) |
seekbar_progress | {stage, processed, total, generated, skipped, errored} — backfill scan. |
seekbar_done | {processed, generated, skipped, errored, durationMs}. |
seekbar_rebuild_progress / seekbar_rebuild_done | Same shape as the build pair, fired by /api/maintenance/seekbar/rebuild. |
seekbar_sprite_ready | {download_id} — fires after a per-row pregenerate succeeds (post-download hook or /regen/:id); the viewer’s hover preview subscribes and flips from pending → ready in place. |
seekbar_sidecar_status | {ok, url?, mode?, version?, error?} — emitted on sidecar boot / respawn / disable. |
seekbar_config_changed | {} — broadcast after POST /api/config touches advanced.seekbar.*; the viewer drops its enabled cache and the maintenance page reloads its KPI strip. |
ai_faces_status | {ok, url?, mode?, state?, error?} — sidecar lifecycle (downloading / starting / ready / relaunching / disabled). |
ai_faces_install_progress | {state, line} — line-by-line output of POST /api/ai/faces/install-deps. |
ai_faces_install_done | {ok, reason?, exitCode?}. |
ai_faces_dim_change | {from, to} — broadcast once after the embedding-dim guard purges stale rows on model upgrade. |
ai_faces_reindexed | {ts} — fired after /api/ai/faces/reindex finishes. |
ai_reindex | {processed, indexed, durationMs} — fired by /api/ai/reindex. |
update_started | {backup} — fired right before watchtower kills the container. |
update_done | {durationMs, kind:'autoUpdate', error?} — error is set when the /api/update pipeline threw (pre-flight or trigger). The SPA’s stall-overlay handler reads this to surface a toast + tear down the spinner. |
rescue_swept | (replaced by file_deleted in v2.8 — rescue sweeper now uses the canonical event so the gallery + footer drop the row in-place). |
rescue_sweep_done | {count} aggregate after every Rescue Mode sweep. |
config_updated | {} |
sessions_revoked | {} |