Add daily_post check: confirm today's post actually published #2

Merged
hannah-vernon merged 1 commit from feature/daily-post-check into dev 2026-08-21 13:28:48 -05:00

Summary

Adds a daily_post check that confirms a post actually published today.

Why

The wp_cron check reports success whenever wp-cron.php returns HTTP 200. That proves the endpoint responds; it does not prove anything was published.

On 2026-08-21 a scheduled post on sqlserverscience.com stayed in future status past its 09:28 slot. Vigilance Sentinel was triggering wp-cron.php every minute throughout, and it answered HTTP 200 every time, so roughly 200 successful triggers happened while the post never went out. The site missed a day with every check reporting green.

The cause was the publish_future_post event being absent from WordPress's cron array. No existing check could see that, because every existing check measures a mechanism rather than an outcome.

What it does

Asks the site what it last published:

"contentFreshness": {
  "enabled": true,
  "url": "https://www.example.com/wp-json/wp/v2/posts?per_page=1&orderby=date&order=desc&_fields=id,date_gmt,title,link",
  "timeZone": "America/Chicago",
  "expectPostByLocalTime": "11:00",
  "timeoutSeconds": 30
}

Before expectPostByLocalTime the check passes, because the day's post is not due yet. From that time onwards it fails until something has been published on the current date in timeZone.

Real output from a live run against sqlserverscience.com, passing:

[+] daily_post  OK  1129ms - today's post is out: post 4913 "Forwarded Records: the Back-Pointer and the Trip Home" at 2026-08-21 09:28

And failing, produced by pointing the same check at the site's oldest post:

[!] daily_post  FAIL  1107ms - no post published today, and it is past 11:00; newest is post 7 "Use DBCC PAGE to identify rows involved in blocking operations in SQL Server" at 2015-03-03 00:15, 4189 days ago

Design decisions

Calendar-day rather than rolling age window. A maxAgeHours window drifts with the previous day's publish time. With a 09:00-09:30 posting window and a 26-hour threshold, a post at 09:30 followed by nothing would not trip until 11:30 the next day, and the margin moves daily. A cutoff answers the question directly: has today's post gone out yet.

Time zone aware. Both the day boundary and the cutoff are evaluated in the configured zone, because publishing schedules are set in site-local time. A test covers a post whose UTC date and site-local date differ, which would otherwise be judged as "today" incorrectly.

No credentials. The REST API exposes published posts anonymously and refuses anonymous requests for scheduled ones, verified: ?status=future returns HTTP 400 unauthenticated. Detecting an overdue future post directly would need an application password, giving a monitoring tool write access to the site.

date_gmt, not date. WordPress emits both without a timezone designator. date is site-local and would be parsed as the monitoring machine's local time. The check fails with a clear message if date_gmt is missing from the requested _fields.

Alert-only. Remediation would need write credentials and turns a monitor into an actor. Out of scope for this tool.

Limitations, stated plainly

  • Reports the outcome, not the cause. It also fires for an empty publishing queue, a post left as a draft, and a deleted post. It cannot tell those apart; all of them mean nothing went out.
  • Assumes a post every day. A site publishing on weekdays only would alert every weekend, so the check should be left disabled there.
  • Reads only the newest post, so it cannot detect a gap earlier in the archive.

Testing

  • 22 unit tests covering response parsing, time zone resolution, the cutoff boundary including the exact-cutoff case, multi-day gaps, and a day boundary that resolves differently in UTC than in the site's zone.
  • End-to-end runs against the live site for both the passing and failing paths, shown above.
  • dotnet build clean, 0 warnings. dotnet test 22 passed, 0 failed.
  • Alerting was not exercised: the SMTP relay is only reachable from the host that runs the scheduled task, so smtp.host was blanked for the local runs.

Notes

  • No new NuGet packages, so THIRD-PARTY-NOTICES.md is unchanged and the zero-dependency constraint holds.
  • InternalsVisibleTo added so tests can reach the parsing helpers and a CheckAsync overload that takes the current time, which is what makes the day-boundary cases testable without waiting on the clock.
  • Version bumped to 1.1.0.
  • The placeholder UnitTest1.cs is removed.
  • ARCHITECTURE.md, README.md, SETUP-GUIDE.md, and samples/example-site.json updated in this commit.

Assumptions

  • Existing configs without a contentFreshness block get Enabled = true from the model default but an empty Url, and the check skips with a success result when no URL is set. Existing deployments therefore behave exactly as before until a URL is configured.
  • The 11:00 default cutoff suits a morning posting schedule. Sites posting later need a later value.
  • TimeZoneInfo.FindSystemTimeZoneById accepts IANA identifiers on Windows as well as Linux, which holds on .NET 6+ with ICU. A Windows identifier such as Central Standard Time also works.
## Summary Adds a `daily_post` check that confirms a post actually published today. ## Why The `wp_cron` check reports success whenever `wp-cron.php` returns HTTP 200. That proves the endpoint responds; it does not prove anything was published. On 2026-08-21 a scheduled post on sqlserverscience.com stayed in `future` status past its 09:28 slot. Vigilance Sentinel was triggering `wp-cron.php` every minute throughout, and it answered HTTP 200 every time, so roughly 200 successful triggers happened while the post never went out. The site missed a day with every check reporting green. The cause was the `publish_future_post` event being absent from WordPress's cron array. No existing check could see that, because every existing check measures a mechanism rather than an outcome. ## What it does Asks the site what it last published: ```json "contentFreshness": { "enabled": true, "url": "https://www.example.com/wp-json/wp/v2/posts?per_page=1&orderby=date&order=desc&_fields=id,date_gmt,title,link", "timeZone": "America/Chicago", "expectPostByLocalTime": "11:00", "timeoutSeconds": 30 } ``` Before `expectPostByLocalTime` the check passes, because the day's post is not due yet. From that time onwards it fails until something has been published on the current date in `timeZone`. Real output from a live run against sqlserverscience.com, passing: ``` [+] daily_post OK 1129ms - today's post is out: post 4913 "Forwarded Records: the Back-Pointer and the Trip Home" at 2026-08-21 09:28 ``` And failing, produced by pointing the same check at the site's oldest post: ``` [!] daily_post FAIL 1107ms - no post published today, and it is past 11:00; newest is post 7 "Use DBCC PAGE to identify rows involved in blocking operations in SQL Server" at 2015-03-03 00:15, 4189 days ago ``` ## Design decisions **Calendar-day rather than rolling age window.** A `maxAgeHours` window drifts with the previous day's publish time. With a 09:00-09:30 posting window and a 26-hour threshold, a post at 09:30 followed by nothing would not trip until 11:30 the next day, and the margin moves daily. A cutoff answers the question directly: has today's post gone out yet. **Time zone aware.** Both the day boundary and the cutoff are evaluated in the configured zone, because publishing schedules are set in site-local time. A test covers a post whose UTC date and site-local date differ, which would otherwise be judged as "today" incorrectly. **No credentials.** The REST API exposes published posts anonymously and refuses anonymous requests for scheduled ones, verified: `?status=future` returns HTTP 400 unauthenticated. Detecting an overdue `future` post directly would need an application password, giving a monitoring tool write access to the site. **`date_gmt`, not `date`.** WordPress emits both without a timezone designator. `date` is site-local and would be parsed as the monitoring machine's local time. The check fails with a clear message if `date_gmt` is missing from the requested `_fields`. **Alert-only.** Remediation would need write credentials and turns a monitor into an actor. Out of scope for this tool. ## Limitations, stated plainly - Reports the outcome, not the cause. It also fires for an empty publishing queue, a post left as a draft, and a deleted post. It cannot tell those apart; all of them mean nothing went out. - Assumes a post every day. A site publishing on weekdays only would alert every weekend, so the check should be left disabled there. - Reads only the newest post, so it cannot detect a gap earlier in the archive. ## Testing - 22 unit tests covering response parsing, time zone resolution, the cutoff boundary including the exact-cutoff case, multi-day gaps, and a day boundary that resolves differently in UTC than in the site's zone. - End-to-end runs against the live site for both the passing and failing paths, shown above. - `dotnet build` clean, 0 warnings. `dotnet test` 22 passed, 0 failed. - Alerting was not exercised: the SMTP relay is only reachable from the host that runs the scheduled task, so `smtp.host` was blanked for the local runs. ## Notes - No new NuGet packages, so `THIRD-PARTY-NOTICES.md` is unchanged and the zero-dependency constraint holds. - `InternalsVisibleTo` added so tests can reach the parsing helpers and a `CheckAsync` overload that takes the current time, which is what makes the day-boundary cases testable without waiting on the clock. - Version bumped to 1.1.0. - The placeholder `UnitTest1.cs` is removed. - `ARCHITECTURE.md`, `README.md`, `SETUP-GUIDE.md`, and `samples/example-site.json` updated in this commit. ## Assumptions - Existing configs without a `contentFreshness` block get `Enabled = true` from the model default but an empty `Url`, and the check skips with a success result when no URL is set. Existing deployments therefore behave exactly as before until a URL is configured. - The 11:00 default cutoff suits a morning posting schedule. Sites posting later need a later value. - `TimeZoneInfo.FindSystemTimeZoneById` accepts IANA identifiers on Windows as well as Linux, which holds on .NET 6+ with ICU. A Windows identifier such as `Central Standard Time` also works.
Add daily_post check: confirm today's post actually published
All checks were successful
Build and Test / build (pull_request) Successful in 1m55s
2f7dd329ed
The wp_cron check reports success whenever wp-cron.php returns HTTP 200.  That proves the endpoint responds, not that anything was published.  On 2026-08-21 a scheduled post on sqlserverscience.com stayed in future status past its slot while wp-cron.php was being triggered every minute and answering 200 each time, so the site missed a day with every check green.  The cause was the publish_future_post event being absent from WordPress's cron array, which no existing check could see.

The new check asks the site what it last published instead of inspecting the mechanism.  It is calendar-day based rather than a rolling age window: before expectPostByLocalTime it always passes, and from that time onwards it fails until something has been published on the current date in the configured time zone.  A rolling window would drift with the previous day's publish time and could stay quiet through a whole missed morning.  The day boundary and the cutoff both use the configured zone because publishing schedules are set in site-local time.

No credentials are required.  The WordPress REST API exposes published posts anonymously and refuses anonymous requests for scheduled ones, so the check reads only what a visitor could read.  Detecting an overdue future post directly would need an application password, which would give a monitoring tool write access to the site.

date_gmt is used rather than date because WordPress emits both without a timezone designator; date is site-local and would be read as the monitoring machine's local time.

Adds 22 unit tests covering response parsing, time zone resolution, the cutoff boundary including the exact-cutoff case, and a day boundary that resolves differently in UTC than in the site's zone.  No new NuGet packages, so THIRD-PARTY-NOTICES.md is unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
hannah-vernon/vigilance-sentinel!2
No description provided.