Measured on real files with quality held fixed (SSIM vs the source, harness self-tested at 1.0000):
hevc_nvenc cq27 produced 115% and 119% of the source video bitrate on two of three files while cq30/31
landed 51-84%; libx265 crf26 landed 33.7/56.7/67.0% at min SSIM 0.988-0.993, i.e. 10-22% smaller than
NVENC at matched quality. Two sources with identical bpp (0.0437) shrank 49% vs 17%, so bpp classifies
files but does not predict the ratio. Notes the audio floor (a 33.7% video stream still yields a 53%
file because AAC dominates what is left) and why VP9/AV1 sources have no headroom (copy the video).
Three silent failures on a network-mounted share: a 403 MB upload that wrote 12.5 MB and returned OK
(the timeout toast arrived minutes later, asynchronously), an os.replace() that never landed while
getsize()/mtime served cached metadata (the destination was still the original H.264 file), and a
byte-count source check that agreed with a length-preserving bad copy. Adds read-back hashing at all
three boundaries (source copy, upload, install) and records the metric traps that hid them: a clean
decode test on wrong pictures, SSIM on near-flat frames, input-seek decoding of open-GOP sources,
stream-copy slices dropping frames, and fixed-name scratch files colliding across concurrent runs.
Audit after the fix: reported=30 landed=29 not-landed=0.
The converted HEVC stuttered in VLC, Chrome and on an iPad (offline) while the source played fine.
Container timing was perfect (5818/5818 frame intervals at exactly 0.040000 s, no dup/backward PTS,
ctts not flat) and a full decode printed nothing. Cross-decoder SSIM found the real defect: NVDEC's
legacy av1_cuvid wrapper returns frames from the wrong timestamps - 106 of 5819 frames, SSIM down to
0.330, deterministically, roughly every 30 frames - and the HEVC output was a faithful encode of those
wrong pictures (min 0.991 against the NVDEC decode, min 0.330 against a software decode). Fix is one
line (-hwaccel cuda -hwaccel_output_format cuda -c:v av1, verified 1.000000 over 1500 frames; vp9_cuvid
unaffected). Adds the content gate (structure AND pictures, defect-shaped thresholds) to the pipeline.
Re-checked against the actual repo rather than the API summary: the old branch
survives as a single commit whose README is the word 'deleted', and it shares no
common ancestor with main's 233 commits. Sharper and fully verifiable, EN+ZH.
1) the-upstream-was-deleted-then-came-back-rewritten (devops, pubDate 2026-10-06)
docker pull hectorqin/reader -> 404; the repo was re-initialised 2026-09-16 and now
carries 233 commits, a TypeScript/Node rewrite, port 5888, SQLite /data, config moved
into the admin UI, and an image on cnb.cool with no tags or releases. Covers telling
dead from rewriting, the migration boundary, and keeping the front door outside the
app image. Facts re-verified live 2026-10-06.
2) putting-a-front-door-on-an-app-you-cant-modify (devops, pubDate 2025-06-24 backdated
into the empty 2025-06 window, updatedDate 2026-10-06 so lastmod stays honest)
The reader-gateway pattern: a second nginx container owning the public port, the
request-time resolver, why a proxy body rewrite cannot touch a client-rendered SPA,
the gate-bounce injection with its pass token, the app's own CSS hook for branding,
and the silent-failure check to run after every app upgrade.
Both: EN + ZH twins, TERMINALS/BANNERS entries appended via append_generator_entries.py
(additive, deletions 0, node --check OK), og/banner generated and measured PASS
(og rows=1 overflow=0 missingHash=0; banner 8 rows, delta 2, overflow 0).
The Phase C1 keystone asset (Story B). Publishes the infra case study:
- architecture: 3 hosts / 162 containers / 78 compose stacks with the per-host split
- incident record: the monitoring panels that lied, and the Passbolt cascade
- how the numbers are re-derived (verify_infra.py) rather than remembered
- no uptime percentage: availability is not measured, so the post says 'monitored'
- hire CTA (case-study convention)
EN + ZH twins, og + banner generated and measured (OG 5 tags/1 row, banner 8 lines delta 2).
Phase C1 keystone (Story B). Draft, not published: draft: true.
- architecture: 3 hosts / 162 containers / 78 stacks, per-host split (86/44, 37/21, 39/13)
- ingress (Traefik + ACME), identity (authentik), monitoring (single Grafana), CI/CD, backups
- incident record: the monitoring panels that lied, and the Passbolt cascade (one missing
config value, three failure modes) — both link to their deep-dive posts
- numbers are re-derivable via verify_infra.py rather than remembered
- NO uptime percentage: availability is not measured anywhere (no probe-based monitor
deployed), so the post says "monitored", not a figure
- DO_NOT_PUBLISH services excluded entirely
EN only + no og/banner yet: not publishable until the ZH twin and images exist and the
owner approves. Local commit only — not pushed.
Re-checked the Sept research against the live site and the repo:
- 53 posts x 2 languages, 127 sitemap URLs, 133/133 internal links 200
- Mark C1 (per-post OG), D1 (search), B2b (draft bank) as done — they were stale as open
- New Phase E (top of the queue): E1 analytics (CF Web Analytics + GSC), E2 main-site ->
blog link (deferred to the www.hoelee.com overhaul), E3 dev.to syndication, E4 ZH nav +
/zh/posts/, E5 og:locale, E6 Cloudflare HTML caching, E7 volume guardrail, E8 hygiene
- New section 4: measured evidence table; conventions section renumbered
Five posts with custom OG + banner and hire CTAs. Three are web3, giving
that category its first posts — /categories/web3/ previously 404'd while the
index advertised it as '0 posts - coming soon'.
Backdated where the work genuinely is older: the foundry-nft and
hardhat-smartcontract-lottery commits date to 2024-08-15..19, so those two
posts fill the empty 2024-10 and 2024-12 archive months with updatedDate
holding the real date. The two 2026-08-19 posts use their real work date.
TERMINALS/BANNERS entries for all five slugs are committed this time (both
generators were clean; diffs verified purely additive).
Backdated into the empty 2025 stretch of the archive (pubDate 2025-07-08 / 2025-03-19)
with the real date kept in updatedDate 2026-09-29 so sitemap lastmod stays honest.
Custom OG + banner for both; no date- or version-pinned prose in either post.
Backdated into the 2026-03-25 -> 2026-09-04 archive gap (pubDate 2026-04-14/05-17/06-24/07-29)
with updatedDate 2026-09-29 holding the real date, so sitemap lastmod stays honest.
Custom OG + banner per post, hire CTA, language switch verified.
- docker/Chrome/CDP gotcha: the container kills the browser, so it never exits
cleanly and no restore mechanism fires (flag, Preferences, managed policy all
verified failing) -> 60s snapshot + replay keeper
- also covers the stale Singleton* lock and custom-cont-init.d permission traps
- custom OG + banner art, EN + ZH, backlog B2 updated
New `devops` post on vetting an upstream dependency before building on it:
the six checks that corrected six assumptions from a 1,306-line architecture
spec — repo vital signs via the GitHub API, grepping for the feature instead
of reading for it (OIDC/SSO/SAML → 0 hits), reading a feature doc's target
branch (white-label lives on `multi-tenant`, not `main`), reading the data
model rather than the feature list (percent/fixed vs five assumed rule types),
the Community-vs-Enterprise tier gate (API tokens free, SSO paid), and whether
the money rail works in-country (selfhost + manual payouts, not Stripe Connect).
Closes with the fallback question and the authentik proxy-provider answer.
- EN: src/content/posts/vetting-an-open-source-dependency-before-you-bet-on-it.md
- ZH: src/content/posts/zh/<same slug>.md (same filename → auto language switch)
- Custom OG (1200x630) + banner (1600x900) art via TERMINALS/BANNERS entries
- pubDate 2026-09-29; build verified 93 pages, 17/17 content checks, listing
order monotonic on /posts/, / and /zh/
Note: the generator maps also carry two entries belonging to parallel
in-flight posts (adding-english-mode…, and the sessions' other slugs);
their post files were left uncommitted.
- migrating-codeigniter-iis-to-openlitespeed (engineering): the two fatal errors
IIS + SSO had been hiding (env() called from Constants.php; parent::__construct
in a controller) — both only surfaced on the first unauthenticated request
- upgrading-codeigniter-46-to-47 (notes): the two undefined config properties the
official upgrade guide cannot warn about (Config\App::$permittedURIChars,
Config\Format::$jsonEncodeDepth)
- both flipped draft:true -> false, pubDate 2026-09-20 -> 2026-09-27 (real publish date)
- generated 1200x630 OG cards + 1600x900 banners for both slugs
- verified before commit: EN+ZH pages build, language switch links both ways,
listing order stays monotonic on /posts, /zh and the homepage, banner terminal
panel centred (gapAbove 102 / gapBelow 104) with no overflow
EN + ZH twins. The recipe for pointing NocoDB at a MySQL/MariaDB database on
another machine: the Docker SNAT source-IP trap (the DB sees the host IP, not
the container IP), a SELECT-only grant restricted to that one host, the async
source-creation API with no job-status route, the auto-sync that makes a manual
table step unnecessary (and the create-table route that makes junk tables), and
what a read-only source costs (no metadata edits, UTC-labelled DATETIMEs).
Also adds the og-gen TERMINALS and banner-gen BANNERS entries for the slug
(keeping the sibling session's entries untouched) and the generated PNGs.
how-i-host-this-blog and automating-cyberpanel-without-the-ui backdated
cleanly by prose but describe 2026-era software (Gitea 1.27 /
act_runner 0.2.13; CyberPanel 2.4.4.1), so an older byline contradicted
the body. Put both back on their real 2026-09 dates and drop the
updatedDate that only existed to hold that date.
Listings render `pubDate` but sorted by `updatedDate ?? pubDate`, so any
post carrying both dates sorted by a date it never displayed. After the
backdate commit the two diverged by years and the list read out of order
("January 7, 2026" above "September 13, 2026", "March 11, 2026" below
"December 10, 2025").
Replace `sortByUpdated` with `sortForListing`, which orders by `pubDate`
and uses `updatedDate` only as a tiebreak. This matches the RSS feed,
which already sorted by `pubDate`.
Verified all three listings (EN, EN homepage, ZH) are monotonically
non-increasing across the full 2024-09 -> 2026-09 span.
11 evergreen posts had no date- or version-sensitive prose, but all
carried September 2026 publish dates, making the archive look like it
started two weeks ago. Spread them from 2024-09 to 2026-03 so the blog
reads as an established publication.
Per post-guideline.md, the true publish date moves into `updatedDate`,
so the sitemap lastmod and listing sort order keep the real recency
while the article displays the long-tail date.
Also fixes pre-existing EN/ZH pubDate drift on how-i-host-this-blog
(EN 09-04 vs ZH 09-06) -- twins must share pubDate.
Posts left untouched pin themselves in prose (e.g. "In September 2026
a Seagate IronWolf 110...", prompt-expiry dates, model release dates).
Held unpublished (draft: true). Records the u003e escaping bug:
authentik renders > in branding_custom_css as the literal text
u003e, so any child combinator produces an invalid selector that
silently matches nothing. Includes the cssRules-based debugging
order and the character safety probe.
Docs: not yet recorded in project-state.md
Two finished posts written from the numerology-report migration work, kept as
draft: true so they build no pages and appear in no listing until published.
Content bank for weeks when there is nothing fresh to write.
- migrating-codeigniter-iis-to-openlitespeed (engineering)
IIS -> OpenLiteSpeed/CyberPanel. The two fatals that only appeared once the
authentik SSO gate was gone, the docroot public/ separation, and the
loopback self-call that becomes a real outbound HTTPS request on LiteSpeed.
- upgrading-codeigniter-46-to-47 (notes)
The two fatal config properties NOT in the official upgrade guide
(permittedURIChars, jsonEncodeDepth), why Composer never merges app/Config,
and the property-diff script that finds the whole class of problem at once.
Both fill the starved engineering (1 post) and notes categories. project-state.md
records them as Step B2b with the publish checklist.
EN + ZH devops gotcha post on debugging MySQL Workbench 26.7.0's failure
to connect to MariaDB. Three patches to Oracle's bundled code, all the
same root cause: `major >= 8` is not a valid MySQL-vs-MariaDB test.
Also adds per-post OG + banner (TERMINALS/BANNERS entries).
The crawler/sitemap mechanics now live in the repo doc, not just in the
commit log: robots.txt is a build-time endpoint that must stay pure ASCII
(no charset on a text/plain response means non-ASCII renders as mojibake),
the policy is allow-all with enforcement deliberately left to Cloudflare,
why writing a real robots.txt demotes Cloudflare's placeholder from
replacement to prepend, and that lastmod/hreflang are derived so they must
never be hand-authored.