v0.12.3
Fixed
-
A plugin whose code is missing can now be switched off.
enabledByOperatoris the standing
instruction to enable a plugin on every boot, and the only way to withdraw it is
POST /api/plugins/{id}/disable. That route answered 404 whenever the plugin was not loaded — which
is exactly the state an install lands in when its package directory is gone: an interrupted update,
or a directory that was never on the data volume. The registry entry survives with its config,
ctx.storageand the enable decision intact, so reinstalling the code brought the plugin straight
back up and no API call could prevent it. The only way out was hand-editingregistry.json:
DELETE /api/plugins/{id}answers 404 in the same state and for the same reason, so uninstalling
was not an escape either.disablerecords intent rather than performing a runtime operation, so it now clears the decision
against the registry when the plugin is not loaded and reports
Plugin {id} is not loaded; it will not be enabled on boot. This is the same reasoning the route
already applied to a plugin sitting inerrorafter a failed restore. 404 is now reserved for an id
with no registry entry at all. Nothing is skipped in the process: there is no runtime in this state,
so no lifecycle hook goes unrun and no worker is left behind. -
Eight settings written to
data/.env.generatedwere silently ignored under the bundled Docker
Compose stack.docker-compose.ymlforwards a variable as- KEY=${KEY:-}so a real host value
reaches the container; with nothing set, that line renders an empty value. An empty value still
occupiesprocess.env, and both lower-priority layers — the project.envand
data/.env.generated— are loaded with dotenvoverride: false, which will not replace a key that
is already present, blank or not.clearBlankEnvexists to delete exactly those blanks before the
files load, but its key list had drifted behind the compose file:AUTO_START_SESSIONS,
BODY_SIZE_LIMIT,API_MASTER_KEY,TRUSTED_PROXIES,CSP_UPGRADE_INSECURE_REQUESTS,
WWEBJS_WEB_VERSION,WWEBJS_WEB_VERSION_REMOTE_PATHandWWEBJS_AUTH_TIMEOUT_MSwere forwarded
but never cleared. Setting any of them indata/.env.generated— a file the first-run header
invites operators to edit directly — did nothing, with no error and no warning.AUTO_START_SESSIONSis the one that got noticed, and it is worth being precise about the shape of
the regression. The bundled production compose gained its forward in 0.12.0 — before that it had
none, so an operator'sdata/.env.generatedsupplied the flag unobstructed, because there was no
blank value to shadow it. (The dev compose has forwarded it since 0.10.0, defaulting it totrue.)
Adding the production forward without the matching clear entry therefore did not relocate an older
failure; it switched off the one route that had been working. The flag resolved to off,
SessionService's bootstrap hook returned before it looked at a single session, and previously
authenticated sessions stayed atdisconnectedwith no engine ever created and a null
lastError. (#981)The clear list is now covered by a test that derives the expected set from
docker-compose.ymland
docker-compose.dev.yml, so a forward added without its clear entry fails in CI rather than
shipping inert. -
Restarting from Dashboard > Infrastructure no longer reloads the page into an error. The restart
modal polls the server and reloads once it answers, but it polledGET /api/infra/health— a plain
ping with no knowledge of shutdown. That endpoint keeps answering200for the entire drain window
and the engine teardown that follows, so the very first poll, three seconds in, was answered by the
process that had just been asked to exit. The modal declared success and reloaded two seconds later,
by which time the old process was gone and the new one had not yet bound its port — leaving the
operator on a 502/503 from their reverse proxy until they refreshed by hand several times.The poll now targets
GET /api/health/ready, which reports503the moment draining starts and
keeps reporting it until both databases answer. The page therefore reloads only once the new process
is genuinely serving, and it waits for the databases too rather than only for the port to open.With the poll now waiting for real readiness, its deadline started to matter for the first time.
It was a fixed 60 attempts at one second, whilePOST /api/infra/restartestimates up to 63 seconds
for a restart that brings up PostgreSQL, Redis and MinIO together — so a full-profile restart could
have reported failure while the stack was still coming up correctly. The deadline is now derived
from the estimate the server already returns, with the old 60 as a floor. (#1019) -
backup.shcould archive a database the application had stopped using. Both scripts resolved
every path from the process environment alone, while the application fills the same settings from
three layers: the environment, then./.env, then<data dir>/.env.generated— the file Dashboard >
Infrastructure writes. An install configured through the dashboard was therefore backed up at the
default paths.That is not reliably loud. A missing database already failed the run, but a database left at a
default path from before the operator switched was archived instead and the run exited 0. A backup
that captured an abandoned database announces itself only during a restore.backup.shwas already
copying.env.generatedinto the archive without ever reading it.Both scripts now resolve through the same three layers, in the application's order, so an explicit
environment value still wins. Only plainKEY=valuelines are honoured: a value containing a quote
or a#anywhere is reported on stderr and skipped rather than guessed at, because a silently
mis-parsed path is the failure being fixed. That is deliberately broader than trailing comments — a
password containing#is skipped too, loudly, and must be passed in the environment. The Postgres
connection detailspg_dumpuses resolve the same way, so a dashboard-provisioned database no
longer needs its credentials restated in the operator's shell. -
cp .env.example .envpinned 23 settings the dashboard is supposed to own. Configuration is
filled from the process environment, then.env, thendata/.env.generated, each supplying only
what the previous layer left unset. Every value shipped uncommented in.env.exampletherefore
became a permanent pin the moment an operator followed the documented setup step: the matching
dashboard control still moved, saved and reported success, while the running value never changed.The blank-forward fix above cannot reach this.
clearBlankEnvruns before.envis read, so it
only clears blanks arriving from the process environment. A value in.env— including an empty
one, since dotenv treatsKEY=as present rather than absent — is never cleared. The copied
DATABASE_TYPE=sqlitewas enough on its own to shadow a dashboard switch to Postgres, leaving the
app quietly on SQLite; and for an operator who had also setDATABASE_TYPE=postgresby hand, the
copiedDATABASE_PASSWORD=then shadowed the password the dashboard had provisioned and the next
production boot refused to start. The dashboard could not warn about either, because the save-time
guard reads a snapshot taken before.envwas loaded and so validated the file value it was writing
rather than the one that would win.All 23 blank-forwarded keys are now commented out with their defaults shown, so copying the file no
longer pins any of them, and a test derives that set from both compose files and fails if one is
shipped uncommented again. Each was checked against its application-level default first; none
changes behaviour silently when absent —DATABASE_USERNAMEhas no application default at all, so
underDATABASE_TYPE=postgresit now fails boot validation naming the variable instead of resolving
toopenwa. One further exception is called out in the file: the dev compose defaults an unset
AUTO_START_SESSIONStotrue, so a dev stack that had inheritedfalsefrom a copied
.env.examplenow gets the dev default it was always meant to have.Five dashboard-managed keys are not covered, because they have no compose forward and so fall
outside both the fix and its guard:POSTGRES_BUILTIN,REDIS_BUILTIN,MINIO_BUILTIN,
DATABASE_SSLandDATABASE_SSL_REJECT_UNAUTHORIZED. Copying.env.examplestill pins those. -
A link posted to a Channel gets a sharp preview thumbnail again (whatsapp-web.js engine).
WhatsApp Web selects a different preview transport for newsletters, andwhatsapp-web.js1.34.7
callsWAWebLinkPreviewChatAction.getLinkPreview(link)with the destination chat omitted — so the
action could not select that transport even though the message was afterwards handed to the
newsletter send job. The preview was built as if for an ordinary chat, which is why the same URL
rendered correctly in a 1-on-1 conversation and arrived blurred or empty on a Channel.The function takes two parameters — read off the live WhatsApp Web build,
getLinkPreviewreports
arity 2 — so the chat now fills a parameter the function actually declares rather than one that
would be ignored. It is applied the way the existing201832fix already is: an exact transform of
the installed dependency, skipped when already present and refused outright on any other shape.
postinstallapplies it best-effort so a local install never hard-fails, while the production image
build runs it strictly, turning dependency drift into a build failure rather than an image that
silently ships without it. The message-edit path is unchanged and still uses the old call. (#1006)