Before
At the end of the month you have to write down what you did, and you dig it out of git history and memory.
After
The dated record is already on the client's own site. You open it.
You look after sites you do not own. This plugin installs on the client's site, keeps a dated record of updates, sign-ins and changes, counts what is quietly filling the database, and gives the client one screen that answers "is my site fine?". Pro adds GA4 traffic, uptime, a branded PDF report on a schedule, and white labelling. The whole thing was written around one constraint: it must not make the client's dashboard slower.
Once a month somebody asks what you actually did, and you reconstruct it from git history, the update screen and memory. Meanwhile the client has a whole WordPress dashboard they never asked for and cannot read. This plugin keeps that record on the client's own site and gives them one screen instead of the rest of it.
Because it is installed on somebody else's site, one constraint sits above the others: it must not make their dashboard slower. Three decisions follow. A module that is off does not load — the registry stores class names rather than instances, and only an active module is ever built. The portal never queries live; every figure comes from a cached snapshot and the screen is server-rendered PHP, with no REST call and no JavaScript. And the heavy work runs on cron behind a lock — building a PDF is the most expensive thing here and it never happens during a page load.
The plugin has seven modules and all of them are in this package: an activity log with an indexed table and scheduled pruning, a database health checker that reports counts first and deletes only what you tick, the client portal, GA4 traffic through a service account, uptime, scheduled PDF reports, and white labelling across the plugin, the reports and — if you want it — the dashboard itself.
The Persian PDF output is genuinely correct, and that was checked by eye rather than assumed: a report was generated, converted to an image and looked at. Vazirmatn is bundled and registered with OpenType layout enabled, and that single setting is what makes Arabic-script letters join — the thing most plugins forget. Output is right-aligned, mixed Persian and Latin lines read correctly, and page numbers are forced LTR so "1 / 2" does not come out as "2 / 1".
Uptime can come from UptimeRobot or Better Stack, or you can point any cron at the heartbeat endpoint. That endpoint is the plugin's only public route and it authenticates with HMAC-SHA256 over a timestamp, compared with hash_equals, inside a one-minute window, with future timestamps refused. More importantly, each signed URL works exactly once. Without that, anyone who saw a heartbeat URL in a server log could replay it and paper over a real outage.
Before
At the end of the month you have to write down what you did, and you dig it out of git history and memory.
After
The dated record is already on the client's own site. You open it.
Before
The client has the entire WordPress dashboard, understands none of it, and rings you.
After
One screen: health score, updates waiting, storage used, recent activity and how to reach you.
Before
The client says the site was down on Tuesday and you have no figure to show them.
After
An uptime percentage, from your monitoring service or from a signed heartbeat that cannot be replayed.
Activate the plugin on the client's site and tick the modules you want on the Client Reporting screen. Whatever stays off does not load its file.
In Settings, choose which role may open the client portal and add your own support details. From that moment the client has a screen.
In Pro, add your branding, set the period to weekly or monthly and name the recipients. The rest happens on cron.
Web design agencies · freelance site maintainers · WordPress support teams · hosts selling a managed plan · digital studios · WordPress consultants · anyone on a monthly maintenance retainer who has to show something at the end of it.
Everything sits inside the plugin. No Composer at runtime, no cloud service, no companion plugin.
A module that is off does not load, the portal reads a cached snapshot, and PDF generation runs on cron behind a lock. Admin CSS and JS load on this plugin's own screens only, and there is no jQuery.
A dated record of core, plugin and theme updates, sign-ins and failed sign-ins, user creation, deletion and role changes, watched settings and content publishing. Every entry carries a severity.
Post revisions, abandoned auto-drafts, trashed content, spam comments, expired transients and orphaned metadata. You see the counts first; nothing is deleted until you tick it and confirm.
One screen with a health score, updates waiting, storage used, recent activity and your contact details. Server-rendered PHP — no REST round-trip, no JavaScript, no spinner.
Managing the plugin and seeing the portal are two different questions. You choose the role that gets the portal, and both decisions can be overridden by a filter.
Sessions, users, page views and top pages through a Google service account. The plugin signs its own JWT, so no Google SDK is bundled with it.
From UptimeRobot or Better Stack with your own key, or from a heartbeat any cron can send. A gap longer than twice the interval you configured counts as downtime.
HMAC-SHA256 over the timestamp, compared with hash_equals, inside a one-minute window, with future timestamps refused. Each signed URL is accepted exactly once, so replaying it cannot hide a real outage.
Weekly or monthly, e-mailed to the recipients you name. Files are written to a guarded folder under a random filename, and downloading one needs both a capability and a nonce.
Vazirmatn is bundled and registered with OpenType layout enabled, which is what joins the letters. Output is right-aligned and page numbers are forced LTR. Checked by converting the PDF to an image and looking at it.
Your agency name, logo and colour on the plugin and the reports, and optionally on the client's dashboard: the login logo, the admin footer, and hiding the plugin from the client entirely.
The GA4 service-account private key and the monitoring API key are declared per module, stripped from every REST payload and rendered as write-only fields. An empty submission means keep what is stored.
More than thirty documented actions and filters in docs/hooks.md. Your module is a class registered through the hd_wlcr_register_modules filter, and only its boot method may register hooks.
| Capability | Detail | Included |
|---|---|---|
| Dated activity log | Core, plugin and theme updates, and theme switches | |
| Sign-ins and failed sign-ins | Failed attempts are rate-limited before they are written | |
| User changes recorded | Creation, deletion and role changes | |
| Watched settings | A list you can change through hd_wlcr_watched_options | |
| Indexed log table | Four composite indexes matching the queries the plugin runs | |
| Scheduled log pruning | Ninety-day retention by default, configurable | |
| Severity on every event | So the important ones can be read on their own | |
| Counts before deletion | Nothing is removed until you tick it and confirm | |
| Cleanups behind a lock | Two admins cannot start the same cleanup at once | |
| Revisions kept per post | Five by default, up to fifty | |
| Server-rendered portal | No REST call, no JavaScript, no spinner | |
| Cached health snapshot | Fifteen minutes, changeable through hd_wlcr_health_ttl | |
| Capped media walk | Fifty thousand files, cached for an hour | |
| Choose the portal's role | Editor by default, overridable by filter | |
| GA4 through a service account | The plugin signs its own JWT; no Google SDK bundled | |
| Uptime from a monitoring service | UptimeRobot or Better Stack with your own key | |
| Signed heartbeat | HMAC-SHA256 over a timestamp, compared with hash_equals | |
| Each signed URL spent once | A repeat of the same URL is refused | |
| Future timestamps refused | Nobody can mint valid URLs in advance | |
| Branded PDF reports | Weekly or monthly, delivered by wp_mail | |
| Vazirmatn bundled | Registered with useOTL so Arabic-script letters join | |
| Page numbers locked to LTR | "1 / 2" does not render as "2 / 1" | |
| Trimmed, vendored mPDF | Hand-written autoloader, no Composer at runtime | |
| Guarded reports folder | Random filename, and a capability- and nonce-checked download | |
| White labelling | Agency name, logo, colour, footer and login logo | |
| Write-only secret fields | Stripped from every REST payload | |
| More than thirty hooks | All documented in docs/hooks.md | |
| Complete Persian translation | pot, po and mo files ship with the plugin | |
| Data deleted only on request | A setting that is off by default |
| Item | Minimum |
|---|---|
| WordPress | 6.2 or newer, tested to 6.8 |
| PHP | 8.0 to 8.3 |
| WordPress cron | Needed for scheduled reports and log pruning; on a quiet site a real server cron is more reliable |
| Uploads folder | Writable — report PDFs are created in a guarded subfolder |
| External services | Only if you want them: a Google service account for GA4, and an UptimeRobot or Better Stack account for uptime. The heartbeat needs no account at all |
Yes. The test is whether you maintain sites you do not own. The plugin is installed on each client’s site separately and keeps the record there. The activity log on its own answers the "what did you do this month" question; your own branding, traffic figures, uptime and a monthly PDF start to matter when you want to hand that report to the client under your name.
That was the design constraint, not a claim added afterwards. A module that is off does not load at all, because the registry stores class names and only an active module is instantiated. The portal does not query live; it reads a cached snapshot whose TTL you can change. PDF generation runs on cron, behind a lock. Admin CSS and JS load on this plugin's own screens only, and there is no jQuery. I am not going to give you a speed figure or a percentage — these are architectural decisions, not the result of a benchmark.
No, because there is nothing to see. Each installation is standalone and holds only that site's data; there is no central hub pooling clients together. If you switch white labelling on and hide the plugin from the client, they see the portal and not your agency screens. The other side of that coin is worth saying too: for the same reason there is no single dashboard showing all of your clients at once.
Seven modules, all active: the activity log, database health, the client portal, GA4 traffic, uptime (from a monitoring service or from the heartbeat), scheduled PDF reports with e-mail delivery, and white labelling. Plus the settings screen, the REST routes, the translations and every hook. A module you switch off is not loaded at all, and apart from GA4 and the monitoring service — both your choice — the plugin contacts nothing.
The free build is 33 files and about 105 KB; the pro build is 404 files and about 1.1 MB. The whole difference is two things carried inside the package: the mPDF library and Vazirmatn in two weights. mPDF ships trimmed — 3.8 MB of source instead of 55 — and is loaded by a hand-written autoloader so that nothing is installed at runtime. Those files are only read when a report is actually built.
Yes, and it was checked by eye rather than assumed: a PDF was generated, converted to an image and reviewed. Vazirmatn is bundled and registered with OpenType layout enabled, and that single setting is what makes Arabic-script letters join — the thing most plugins forget, which is why their output comes out disconnected. The output is right-aligned, tables and mixed Persian-Latin lines read correctly, and page numbers are forced LTR so "1 / 2" does not come out as "2 / 1".
You need a Google Cloud service account and its key file, and somebody with admin access to the GA4 property has to add that service account's e-mail address as a viewer. Usually that person is the client, so this is the one part you cannot switch on single-handed. The plugin signs its own JWT with the analytics.readonly scope and bundles no Google SDK; the private key is a write-only field and never comes back in a response. If you cannot get the access, leave the module off — the rest of the plugin works exactly as before.
Yes. Point any cron or monitor at the heartbeat endpoint and the plugin works out availability from the gaps between pings; a gap longer than twice the interval you configured counts as downtime. The URL is signed with HMAC-SHA256 over a timestamp, compared with hash_equals, the timestamp must be within about a minute of the server clock, future timestamps are refused, and each signed URL is accepted exactly once. That last part is the one that matters: without it, anyone who saw a heartbeat URL in a server log could replay it and paper over a real outage. The guide has a ready-made curl command.
Nothing, unless you asked for it. There is a "delete data on uninstall" setting and it is off by default, because removing a plugin to reinstall it should not cost you a year of activity log. Deactivating only clears scheduled events and caches and never touches stored data. Switch that setting on and uninstalling removes both tables, the settings and the generated PDFs.
Plugin bug fixes, installation and setup guidance — including getting GA4 and the heartbeat working — and answers to usage questions. Customisation, new feature development and problems caused by other themes or plugins fall outside support and are agreed separately.