Skip to content

Promote develop to main: the gallery conversion damage, and operations as recurring work - #56

Merged
ptr727 merged 5 commits into
mainfrom
develop
Aug 8, 2026
Merged

Promote develop to main: the gallery conversion damage, and operations as recurring work#56
ptr727 merged 5 commits into
mainfrom
develop

Conversation

@ptr727

@ptr727 ptr727 commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Promotes develop (7b1f8ff) to main (78d36e4). Five pull requests: one code and content change, and four state-file updates, two of which had sat open since 2026-08-05 and 2026-08-07.

#55 Restore gallery captions and items the conversion flattened
#54 Record operations as recurring work, not as a finished milestone
#37 Record the transport retest, retire the #33 entry, and hold the main publish
#48 Record media integrity in the State table
#57 Record M7a as done, since production is serving a release

The one substantive change

#55 found conversion damage nothing could see, and closed the hole that hid it. A gallery is a flex row whose column widths come from .gallery-cols-N figure, so anything inside it that is not a figure gets no width and is laid out as one more item in the row. Three shapes of that survived the WordPress conversion:

Shape in the markdown Count What the old platform had
Caption text trailing the last figure shortcode's }} 11 <figcaption class="blocks-gallery-caption"> — a caption for the set
A bare ![](…) image 2 <li class="blocks-gallery-item"><figure>
A linked [![](…)](…) image 8 the same, with the <a> inside the figure

No media check could reach any of it, and that is the interesting part rather than an excuse: every one reasons about a URL, and content misplaced inside a gallery resolves, is linked, and is reached exactly as correct content is. The defect is purely structural.

Each shape was settled against the captured live site rather than inferred from the markup, which mattered most for the first. Read from the source alone, the obvious fix is to move the trailing text into the last figure's caption parameter — which is what the review proposed, three times. The capture shows all eleven were blocks-gallery-caption elements, matching the eleven trailing texts one for one with nothing unaccounted for on either side. That fix would have attributed a caption describing four images to whichever image sorted last, and it would have looked correct.

So the gallery shortcode takes a caption of its own and renders its container as a figure, since figcaption is only valid as a figure's child. check-url-parity.py gains the gate, which parses the built pages because nested <figure> defeats a regex, and it found the third shape itself, on a post neither the review nor the issue had mentioned.

The state file

TODO.md now separates work that ends from work that does not. Recurring operations holds the periodic log review and the off-host log pull, both of which had been sitting among the one-off migration tasks where finishing the list would have read as done while neither had ever run on its cadence. The State table gains an Operations row reading not started, and a Media integrity row.

Several entries are closed out against what actually happened rather than what was expected:

  • HUGO_BASEURL on the production environment held the live WordPress address. The deploy workflow builds with that value and points the live check at it, so a production deploy would have baked .com into every canonical tag, feed link and sitemap.xml, then sent 1,245 requests at the live site to verify it. Now the interim .net name; the revert at M7b is recorded as unfinished.
  • robots.txt is a loss at the cutover, not a gap. The live .com blog serves one today carrying a Sitemap: line, and this site emits none, so M7b moves a site that has had crawl directives for years to having none. Recorded as deliberately undecided with three options.
  • Nothing checks that media survived the trip to the server. The legacy media list is gated at build time against files on disk, and the live check never requests an image, so a media tree lost between build and serve is caught by neither.
  • #33 is retired, closed 2026-08-06. Declare what the VPS keeps is retired, answered by OPERATIONS.md now naming blog-prune-releases.timer, its cadence, and its count.
  • A ProjectTemplate#567 row was dropped rather than carried. That issue closed 2026-08-05, before the branch proposing it was last touched, and its claim — that an agent cannot trip the production ref gate because the permission classifier refuses the dispatch — was falsified on 2026-08-08 by a production deploy that dispatched and completed.

One correction the review of this promotion produced

The first round on this pull request caught TODO.md contradicting the description of its own promotion: the file still said production served 0000-placeholder and that the first deploy was pending, in three places, while it had deployed and was serving 20260808-041050. #57 fixes all three with what was measured rather than what was expected, including the host side's independent verification, and is in this promotion.

Worth naming how it was caught, since nothing here runs that check: it came from comparing the file against the description written about it.

Verification

The live check was run against the local mirror on this exact tip, which is the half of the URL contract CI cannot reach:

gallery: 19 pages with galleries, 0 stray nodes inside one
PASS - the built site honors the URL contract

==> served by mirror-staging
==> rules from release 20260808-135504
PASS - 1245 URLs honored

assets at 1049 and orphans at 98 are both unmoved from before the gallery rewrite, which is the evidence it lost no media and orphaned none. Build clean under hugo --gc --minify --panicOnWarning.

Every one of the five pull requests was driven to a clean round with the hub's scripts/pr_review.py, status exiting 0 on each.

Not in this promotion

No publish. publish-release.yml is workflow_dispatch only, so merging here ships nothing. 1.0.11 remains the newest release from main, and the item holding the next one for the soak is in this promotion rather than being acted on by it.

No production deploy. Production currently serves 20260808-041050, which predates every commit here, so it is running the pre-fix galleries. That is a rendering correction rather than a regression, and redeploying is a separate decision.

One known staleness deliberately left. The hub findings table says three findings are open and lists ProjectTemplate#552, which is closed — but its fix is on the hub's develop rather than main, and this file's own rule is to read main as ground truth, so the finding still binds. How the table should represent that deserves its own change and its own reasoning.

Merge with a plain merge commit. Do not pass --delete-branch, which on a develop -> main promotion deletes develop.

🤖 Generated with Claude Code

ptr727 and others added 4 commits August 7, 2026 22:00
* Restore gallery captions and items the conversion flattened

Closes #52.

A gallery is a flex row whose column widths come from `.gallery-cols-N figure`,
so anything inside it that is not a figure gets no width from that rule and is
laid out as one more item in the row. Three variants of that survived the
conversion, and no gate could see any of them: every media check reasons about a
URL, and misplaced content resolves and is linked exactly as correct content is.

Each was verified against the captured live site before being changed rather than
inferred from the markup, and the first is why that matters. Eleven captions in
the FLAIR post were written as text trailing the last figure shortcode. Reading
only the source, the obvious fix is to move the text into that figure's caption
parameter, which is what the review suggested. The capture shows all eleven were
blocks-gallery-caption elements, captions for the set, so that fix would have
attributed a caption describing four images to whichever one sorted last and
would have looked right. They move to a caption on the gallery instead.

The other two were gallery items the conversion emitted as plain markdown, two
bare images and eight linked ones, against li.blocks-gallery-item > figure in the
original. Both become figure shortcodes, the linked ones through the parameter
the theme already has, which puts the anchor back inside the figure where it was.

The container becomes a figure, because figcaption is only valid as a figure's
child. The reset already sets figure { margin: 0 } and the theme already styles
figure > figcaption, so a set caption needed one rule: a full-width flex basis,
without which it packs onto the end of the last row and reads as a caption for
whichever image it lands beside.

check-url-parity.py gains the gate that would have caught all of this, parsing
the built pages rather than matching text, since nested figures defeat a regex.
Demonstrated failing before being trusted: against the pre-fix content it reports
27 stray nodes and exits 1, and it distinguishes the shapes rather than counting
them, naming bare text and each stray element type.

The gate found more than the review did. The linked-image variant is on a post
neither the review nor the issue mentioned, and it was found only by running the
check across all 19 pages that carry a gallery.

Verified through make-release.sh and against the local mirror: PASS - 1245 URLs
honored, with assets at 1049 and orphans at 98, both unmoved, so no media was
lost or newly orphaned by the rewrite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Reject a gallery page by the class token, not by one spelling of it

Both from review on #55.

The cheap reject read `class="gallery`, which assumes the quotes survive and that
the token sorts first. Minification drops the quotes around a value that does not
need one and says nothing about class order, so `class=gallery` and
`class="foo gallery"` both skipped the parser entirely and the gate passed
vacuously on a page whose markup was merely spelled differently. It matches the
bare word now, which cannot skip a page the parser would find, since the parser
requires the class token and a page carrying it always contains the string.
Matching a page that only mentions the word costs one parse.

The printed count is now taken from what the parser found rather than from the
reject, so the broader test does not turn "pages carrying a gallery" into "pages
the word appears on".

The shortcode comment claimed a set caption needed no rule of its own, while this
branch adds one. It does need it, and for a reason worth keeping: the gallery is
a flex container, so without a full-width basis the caption packs onto the end of
the last row and reads as a caption for whichever image it lands beside. The
comment now says so and says not to remove the rule as redundant.

Verified on all three spellings, quoted, unquoted, and reordered, each carrying a
known stray node: all three are now detected, and the first two were not before.
The site itself is unchanged at 19 pages with galleries and 0 stray nodes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Name the invariant the gallery check tests, not the shapes it has met

From a suppressed finding on #55, which was right: the docstring said "both
conversion artifacts" while the README and the description say three. It was
written after two were known and never revised when the check itself found the
third.

Rewritten to say what it actually tests. Three source patterns reached it and
they arrive as three different HTML shapes, so an enumeration is a list to extend
every time the conversion surprises us again. The invariant is that a gallery
holds figures and its own caption, and that is what covers the shape nobody has
thought of yet, which is how the third was found after the first two.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Record why the gallery scanner has no handle_startendtag

From a suppressed finding on #55, which reasoned correctly from the code and
reached the wrong conclusion about the base class. HTMLParser forwards a
self-closing tag to handle_starttag and then handle_endtag, so `<br/>`, `<br />`
and `<img/>` are already reported as stray children and already leave the depth
balanced. Verified on those three spellings and on a self-closing non-void
`<figure/>`, which enters and exits correctly and lets the `<p>` after it be seen.

Adding the override the finding asks for is what would break this, by counting a
pair the base class already splits. That is worth a comment rather than a reply
nobody reading the file later will find.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Say what a gallery failure found, rather than calling it missing

From a suppressed finding on #55. The shared summary printed
`FAIL gallery: N missing` for a check whose findings are nodes that are present,
which is the entire complaint about them.

Each label can now name its own noun, defaulting to "missing" so the four older
checks are unchanged and a check added later reads the way they do unless it says
otherwise. Verified by forcing a failure: `FAIL gallery: 15 stray nodes`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Record operations as recurring work, not as a finished milestone

The backlog listed the log review and the off-host log pull among the
one-off migration tasks, so a reader finishing that list would read the
migration as done while neither duty had ever run on its cadence. Both
move to a Recurring operations section that says outright that everything
above it ends and this does not, and the State table gains an Operations
row so the summary stops implying the pipeline is the whole system.

The open decision behind them is that the tooling has no home in git.
vps-backup-pull, its systemd units, and the variables naming both ends of
the copy exist only on the Proxmox host, which is the host the backup runs
from, so losing it loses both the copies and the means of making them.
Two candidate homes are recorded without choosing between them.

The same question covers the fourteen migration scripts in the capture
directory, and one of them already shows the cost of leaving it open:
build-redirects.py exists in three places, the two capture copies
identical to each other at 115 lines and this repo's maintained copy at
225. The capture is not a git repository, so nothing detects that.

Two open questions are closed with measurements rather than assumptions.
The production environment's HUGO_BASEURL held the live WordPress address,
which the deploy workflow both builds with and points the live check at,
and it is now the interim name. And the first production deploy does not
fix the robots.txt 404 the VPS agent flagged in its 22.10: hugo.yaml sets
no enableRobotsTXT so the site emits none at all, while sitemap.xml is
emitted and becomes fetchable at that same deploy.

The VPS production row said the resource was deliberately disabled, which
its own file contradicted twenty lines later.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Record the robots.txt cutover loss and the media gate gap

The gallery caption entry under Open decisions is gone: #55 answered the question
it posed, against the captured live site, and closed #52 with it.

Two findings from the VPS agent's §23.3 and §24.3 replace it.

The robots.txt entry described a gap on the interim name and missed that the
cutover makes it a loss. The live .com blog serves a robots.txt today carrying a
Sitemap: line, and this site emits none, so M7b moves a site that has had crawl
directives for years to having none and takes the sitemap pointer with them. That
side found it from the outside, will not put a file in this bundle, and has made
it block step 1 of the M7b checklist rather than be discovered after it. Recorded
as deliberately undecided with the three options, and Open decisions points at it
rather than restating it, so the two cannot drift.

Nothing checks that media survived the trip to the server. A 3,095-request gate
run fetched no image at all, which prompted the question of whether the legacy
media list is wired in. It is, at build time, against files on disk. The live
check requests pages and redirects and never an image, so a media tree lost
between the build and the server is caught by neither, the build having passed
before the loss and the live gate never asking.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…publish (#37)

* Record the transport retest and what #33 still owes

* Retire the #33 entry and hold the main publish for the soak

The retest issue closes on the prerelease dispatch rather than on a production
deploy, so the entry naming a production deploy as its fourth check is answered
rather than outstanding. #33 has since closed, on 2026-08-06, which confirms it.

The main publish is added as its own item, gated on the soak rather than on the
mechanism, which is proven. 1.0.11 is still the newest release from main and
still predates every deploy change, so the next release from main is the first
that would describe a site serving its public address.

Rebased onto develop and reconciled with what landed while this sat open.

The hub row this branch added for ProjectTemplate#567 is dropped. That issue
closed on 2026-08-05, a day before this branch was last touched, so the row was
already wrong when written. Its claim has since been falsified outright: it said
an agent cannot trip the production ref gate because the permission classifier
refuses a production-labeled dispatch, and a production deploy was dispatched
and completed on 2026-08-08.

The `Deploy production once` bullet keeps develop's version rather than this
branch's. This branch still described that environment as never having run, and
it has now served a release.

The `Declare what the VPS keeps` bullet goes, which the earlier commit dropped
without saying why. It asked for the host's timer and its count to be written
beside the ownership line, and `OPERATIONS.md` now names
`blog-prune-releases.timer`, that it runs daily, and that it keeps ten bundles
per environment. The item is answered rather than abandoned.

The main publish item pointed at "the daily 404 watch above", which moved below
it into Recurring operations, so it names that section instead. A pointer by
position is the same defect a review already flagged on this branch once.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The table said nothing about whether the conversion lost anything, which is
the one question a migration's record has to answer and the one a reader
cannot check for themselves.

Rebased onto develop and reconciled with two things that landed while this sat
open. The volatile counts it originally carried in the Content and media row
are gone, because develop deliberately replaced counts that content additions
move with what they represent, and this row would have reintroduced 514 pages
against that decision. And the claim is narrowed from "closed" to closed for
the files themselves: the image loss was 19 and is restored, but #55 later
found the conversion had also flattened how galleries present media, in three
shapes across three posts. That was not a file loss and no file was missing,
which is exactly why nothing saw it for so long, and it is now gated.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 8, 2026 13:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Promotes develop to main, carrying a gallery-structure repair (restoring gallery set captions and converting stray gallery children into proper figure shortcodes) and adding a new HTML-parsing gate to detect future gallery-structure regressions, alongside state-file updates that reframe ongoing operational work as recurring rather than “done”.

Changes:

  • Update the gallery shortcode to render a <figure> container and support a gallery-level caption, with CSS to force the set caption onto its own full-width row.
  • Extend checks/check-url-parity.py with a new gallery structure check that parses built HTML and fails on non-figure/figcaption direct children inside galleries.
  • Update TODO.md state tracking to add/clarify Media integrity and Operations, and to restructure “Recurring operations”.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
TODO.md Updates state tracking (including a new Operations row and recurring-ops section) as part of the develop→main promotion.
layouts/shortcodes/gallery.html Changes gallery container semantics to <figure> and adds a gallery-level caption output.
assets/css/extended/custom.css Adds styling so gallery-level <figcaption> spans full width in the flex layout.
checks/check-url-parity.py Adds an HTMLParser-based gallery structural gate and adjusts failure nouning for gallery failures.
checks/README.md Documents why/what the new gallery check covers.
content/posts/2022/10/30/installing-flair-smart-vents-to-keep-room-temperatures-balanced.md Converts trailing gallery captions into gallery caption= parameters and removes stray trailing text.
content/posts/2020/06/21/moving-from-unraid-to-proxmox-ve.md Converts linked markdown images inside a gallery into figure shortcodes using link=.
content/posts/2020/02/02/recovering-the-firmware-on-a-supermicro-bpn-sas3-846el1-backplane.md Converts bare markdown images inside a gallery into figure shortcodes.
Suppressed comments (1)

TODO.md:32

  • This bullet says the interim production hostname is still serving 0000-placeholder, which conflicts with the PR description’s claim that production currently serves 20260808-041050. Please confirm which is correct and update this item accordingly so the recorded dependency order matches reality.
- **Deploy production once, to a name that is not the live one.** The host side is done and the interim name is live: `blog.insanegenius.net` answers `200` unauthenticated, on a Let's Encrypt certificate issued 2026-08-07, serving `0000-placeholder`. The VPS agent calls this M7a, and M7b is the `.com` cutover. Three items belong to this repo before the first production deploy, all detailed in the VPS agent's §19 and §20. That file is not in the repository, so pull it first per [`OPERATIONS.md`](./OPERATIONS.md) "The Channel Between the Two Sides":

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread TODO.md Outdated
@ptr727

ptr727 commented Aug 8, 2026

Copy link
Copy Markdown
Owner Author

The suppressed finding on this round is the same defect as the resolved thread — TODO.md:32 describing the interim hostname as serving 0000-placeholder — and is answered by the same change, #57. Recording it here because a suppressed finding has no thread to resolve.

Found by review on the promotion #56, which caught the file contradicting the
description of its own promotion. The State table and the dependency list both
still said production served 0000-placeholder and that the first deploy was
pending. It deployed on 2026-08-08 and serves 20260808-041050.

Three places said it, and all three are now what was measured rather than what
was expected. The State row records the release and that the host side verified
it independently, 9/9 unauthenticated with the built baseURL read from the
deployed bytes rather than from this repo's config, across a 3,095-request gate
run. The production item leads with M7a done and reframes its sub-items as where
this repo stands against M7b, two owed and one answered, rather than as work
before a first deploy that has happened. And the robots decision is now before
the cutover rather than before the first deploy, with its 404 measured after the
deploy rather than predicted before it.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 8, 2026 13:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

@ptr727
ptr727 merged commit 1aec5fc into main Aug 8, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants