Releases: Digitizers/digitizer-pro-tools
Release list
1.37.0
Agent Log: a site can now silence a writer it does not care about.
Reported from a staging site: every WP-CLI run logged Elementor rewriting its
own Default Kit settings — three rows in a twelve-row log.
The obvious fix would have been to skip saves with no field diff. There is no
such save: update_metadata() refuses an identical write before any hook
fires, comparing the stored value with === and returning false. So anything
that reaches this module did alter the row, and those entries are true.
They are simply uninteresting, and only the site can say which of its writers
are. The object type is not the criterion either — filtering
elementor_library would fix one plugin's habit, leave the pattern open for
every other plugin that writes settings on load, and hide a genuine edit to an
Elementor template.
add_filter( 'dpt_agent_log_record', function ( $record, $entry ) {
return ( 'elementor_library' === $entry['object_subtype'] ) ? false : $record;
}, 10, 2 );
It runs when the request ends, so the entry carries the channel and the name
of the application password that made the change. And on a network it runs
inside the site the change happened on, so a callback may read that site's own
options to decide.
Nothing is filtered by default. What is absent from this log is meant to
mean it did not happen over an API.
The same filter is in the standalone plugin as
digitizer_ai_agent_log_record. This module stands down whenever that plugin
is active, so only one of them ever runs.
Design note: docs/superpowers/specs/2026-09-02-agent-log-noise-filter-design.md.
1810 assertions pass.
1.36.0
Copy URL: the copy-this-page-address widget as one module.
A new module (off by default) replaces a functions.php snippet and the
three-widget Elementor construction around it - a form carrying the address
through a dynamic tag, an absolutely positioned heading acting as the button,
and a raw HTML widget with the copy script.
[digitizer_copy_url] renders the whole widget: a read-only field with the
current page's address and a one-click copy button in one flex row. Clicking
anywhere copies through the clipboard API (with a fallback for contexts that
lack it), the button confirms and reverts, labels are settable per shortcode,
and the styling inherits the theme's font and colours with --dpt-cu-* custom
properties for overrides. Any number of instances per page.
[digitizer_geturl] keeps the snippet's contract for existing pages, hardened:
scheme and host come from the canonical home URL (correct behind a reverse
proxy, and without doubling a subdirectory install's path), Hebrew slugs are
decoded for reading while escaped delimiters, literal plus signs and anything
that is not valid UTF-8 keep their percent armour, literal quote characters
are encoded rather than deleted, and the bare shortcode output is escaped at
its HTML boundary so an ampersand never turns into a character reference.
Five Codex review rounds, all findings fixed with regression tests
(29 assertions for this module).
1.35.0
Site Tweaks learns to lock Elementor pages to the Elementor editor.
A new toggle (off by default) stops the recurring support call where a client
clicks "Back to WordPress Editor" on an Elementor page, saves, and the site
renders the Gutenberg content instead of the Elementor layout. Users without
the bypass capability (manage_options by default, filterable) are sent from
the native editor straight to Elementor for pages built with Elementor, edit
links on the post lists and admin bar point directly there, and the switch
button is hidden from them.
Administrators are never affected. Trash, untrash and the Elementor editor
itself always pass through - the action=edit condition is the redirect-loop
guard. A user Elementor itself refuses (a role excluded in Elementor Pro's
Role Manager) keeps the native editor and the switch, so nobody is ever left
with no editor at all. A per-post exemption filter reaches every surface,
including the hidden switch. Deactivating Elementor makes the whole feature a
no-op.
Filters: dpt_st_elementor_lock_bypass_cap, dpt_st_elementor_lock_enabled,
dpt_st_elementor_lock_hide_switch.
1.34.0
Content Control learns the standalone plugin's global restrictions.
Until now the module restricted content per page, per menu item, or the whole
site behind login. This release adds restriction rules as their own entities:
each rule picks content (post types, selected IDs, taxonomies, archives, page
templates - combined with AND/OR and an optional group), names an audience
(logged-in/out, with any/match/exclude role matching), and decides what
refusal looks like - redirect to login/home/a URL, replace with a page, or
show the message, with per-archive hiding or replacement, forced hiding from
search results, and teaser excerpts above the notice.
Classic widgets gain visibility controls, and the [dpt_restrict] shortcode
gains excluded_roles, inline and class. Per-page settings, menu visibility and
whole-site protection keep working exactly as before - and a per-page setting
always wins over a global rule.
Manage the rules under Digitizer Pro Tools → Content Control → Restrictions.
Order is priority: the first matching rule wins.
PR #43 - 12 Codex review rounds, 22 findings addressed; the cross-page
pagination-totals limitation of post-query filtering is tracked as #44.
1.33.0
The Agent Log module recognises the standalone plugin under its new name.
WordPress.org pended the standalone's submission because "AI Agent Activity
Log" begins with a generic term and reads as a description rather than a name.
It is now Digitizer AI Agent Log — the same fix Update Policy took before
it was approved as Digitizer Update Hold.
Both names count as the standalone being present. The first never reached the
directory, but it did reach the machines it was tested on, and a site still
running that build has to keep standing this module down — otherwise it
quietly ends up with two sets of listeners on one set of hooks, which is the
whole thing the check exists to prevent.
The notice on the Modules screen names whichever of the two that site actually
has, so it never sends anyone looking for a plugin that is not installed.
1570 assertions pass. Hebrew catalog at 702 strings.
1.32.1
A fix to the preview screen that shipped in 1.32.0, and the document that says
how to use it.
The preview could mislabel a site's own field
DPT_RB_Fields::compat() records names without the targets they landed on —
deliberately, so a second definition of a key cannot report the same alias
twice. The preview marked every field row whose name appeared in that list,
which is not the same question. A site that defines reading_time on pages,
while the compatibility layer adds reading_time to posts, saw its own field
labelled as somebody else's — on the screen used to decide whether to
deactivate a working plugin.
The names are now listed on their own instead, and the list says what it
actually holds: names the replaced plugin published, and jet_-prefixed
aliases invented when a field's own name is one a core REST property already
owns. Only the first kind is API surface anything outside the site can depend
on.
docs/rest-bridge-deployment.md
The order to deploy REST Bridge in, which matters more than it looks:
- Enable the module before deactivating
digitizer-api-extensions. The
module ships disabled and stands down while the old plugin is present, so
that order leaves no gap; the reverse leaves the site with neither, and
requests arriving in that window get 404s or fields missing. - Compare fields, not routes.
faq/bulkis dropped by design and
faq/infois replaced by/info, so a like-for-like route comparison lands
every deployment in the "exposes less — stop" case on a difference that was
intended. GET /wp-json/digitizer/v1/infoneeds authenticating; it requires
edit_postsand answers 401 anonymously, which at the verification step
looks exactly like a deployment that failed.
Seven review findings went into this, six of them in the instructions rather
than the code.
1.32.0
Two releases' worth of changes: 1.31.0 was merged but never shipped, so a site
on 1.30.0 gets both.
REST Bridge: see what it would expose before switching it on
REST Bridge finds the fields it exposes by reading the definitions JetEngine
already stores, so until something reads them nobody knows what the module
will put on the API. The /digitizer/v1/info endpoint answers that, but only
for a module already running - and the module stands down while the plugin it
replaces is active. So the only way to look was to deactivate a working plugin
on a client's site and find out.
Digitizer Pro Tools > REST Bridge Preview now runs the module's discovery
and registration without registering anything, and reports what it found:
every field with its target and type, compatibility names marked, everything
skipped with the reason, the Rank Math keys, and the routes. Nothing on the
screen changes the site, and it works while the old plugin is still active -
which is the point of it.
The rehearsal suppresses exactly one call, register_rest_field(). Every
discovery, collision check, refusal and diagnostic runs as it always does.
The info endpoint gains rank_math_fields alongside its existing boolean:
those twelve keys are registered as post meta rather than REST fields, so an
agent told only rank_math: true was left to guess what that added.
Agent Log stands down for the standalone plugin
The Agent Log module has been extracted into a plugin of its own for
WordPress.org - AI Agent Activity Log. A site running both would keep two
logs of one thing, in two tables, written by two sets of listeners on the same
hooks. When the standalone is active this module now registers nothing and
says why on the Modules screen, the way Update Policy already does for
Digitizer Update Hold.
Under the hood
1564 assertions pass. Hebrew catalog at 700 strings.
v1.30.0
New module: Agent Log
A log of what the automations did to this site — not a general activity log.
Digitizer sites are edited by software as much as by people. When a page is wrong, the first question is not "what happened here" but "did an agent touch this, and what did it touch". A general log answers that only by burying it.
So this one records anything that arrived over the REST API, WP-Cron, WP-CLI or XML-RPC, and records nothing at all for a change made by a person in a browser — including a save in the block editor, which reaches WordPress as a REST request and is told apart by its authentication. Reads are never recorded either: an API client that polls would fill the table in a day and drown the writes that were the reason to look.
- The fact, never the values. Each row names who, what, when, which channel, which application — and the names of the fields that changed. Never their contents, so the log does not become a second copy of the site's content, and there is nothing in it that hurts if it leaks.
- One row per object per request. A post update with eight meta keys is one row with eight names, not nine rows.
GET /digitizer/v1/activity, requiring the capability to manage options, with the same pagination headers WordPress collections use. No route deletes anything — a log that can be erased through the API is a log an attacker erases on the way out.- A screen, with filters for channel, object type and date range, reading in the site's own timezone.
- Kept for 30 days or 20,000 rows, whichever comes first. Both limits are filterable, and either can be switched off on its own.
- Switching the module off leaves the log where it is. Only uninstalling the plugin removes it — deactivating is reversible and routine, and a log destroyed by it would be gone exactly when someone was investigating.
It ships disabled, like every module here, and it stands down for nothing: it complements a general activity log rather than replacing one, and a site may run both.
Also in this release
Nothing else changed. The plugin now carries nineteen modules.
Upgrading
No settings change, nothing to migrate. The table is created the first time the module is switched on, not on upgrade — a module nobody enabled leaves nothing behind.
v1.29.0
Update Policy answers in Site Health
A hold is a missing update, and a missing update is what someone sees when they go looking for a problem. The module already explained itself on the Updates screen and the dashboard. Site Health is the third place people ask, and it was silent there.
One entry, three answers:
- A release is being held — reported as passing, not as a fault. Names the release, the date this site first saw it, and the date the hold ends.
- Nothing is being held — also worth saying. "The policy is on and holding nothing" is the reading that stops somebody hunting for a hold that was never there.
- The hold is set to zero days — raised as a recommendation. It is the one setting that contradicts the Modules screen: switched on, doing nothing it claims to.
On a network the entry appears only where core updates are actually administered. A subsite decides nothing here, so it is told nothing.
The hold gets the last word on unattended updates
Major core auto-updates are off by default, so the module's refusal has always been belt and braces — but a site that turned them on should not quietly bypass a hold. That refusal now runs late.
Other update plugins hook the same decision at a high priority precisely so their answer is the last one, and a plugin arguing for an update beats one arguing against it by merely running later. The hold would have survived that argument anyway, because the held release is removed from the update offer and there is then nothing left to authorise. This makes the agreement explicit rather than incidental — while still leaving a site able to take the last word back from its own mu-plugin.
Under the hood
The test harness now models hook priority and removes a named callback rather than every callback on a hook. Without that, a test of the priority would have passed whatever number the module used; the assertion that carries the weight now puts a competing filter against this one and watches which wins.
No settings change, nothing to migrate.
v1.28.0
Update Policy knows the standalone by either name
The Update Policy module stands down when the standalone plugin is installed — a site running both would put two filters on one update transient and show two screens saying the same thing. That standalone was renamed Digitizer Update Hold for the WordPress.org directory, and the module now recognises it under either name.
- Both spellings count. A site that adopted the standalone before the rename still has the old class. Recognising only the new one would have silently un-stood-down exactly those sites — the failure the stand-down exists to prevent.
- The note names the screen that is actually there. Two things vary: which build is installed, and whether this is a network. Naming either wrongly sends someone hunting for a menu item their site does not have. The sentence now describes the installed build, under Network Admin > Settings on a network and under Settings on a single site.
Packaging
msgfmt had left a byte-for-byte copy of the Hebrew catalog at the repository root, which nothing loads and every release ZIP carried. Removed, and the command that produced it now discards its output. This download is 86 KB lighter than the last.
No settings change, nothing to migrate.