docs(review): apply batch-01 consultant fixes and update review status
Deploy / build (push) Successful in 23s
Deploy / build (push) Successful in 23s
This commit is contained in:
1 parent
cd6511a0b3
commit
69ce61a932
24 files changed
+119
-8049
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# 全站文章顾问审阅状态总控台 (Audit Tracker)
|
||||
|
||||
> **最后同步时间**:2026-10-27 06:11:29
|
||||
> **最后同步时间**:2026-10-27 06:14:26
|
||||
> **全站总文章数**:91 篇 | **已审批完成**:91 篇 (100.0%) | **待审阅**:0 篇
|
||||
|
||||
## 一、操作命令速查
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,22 @@
|
||||
# `blog.hoelee.com` 外部顾问审阅报告 — Batch-01
|
||||
|
||||
> **对应资料包**:`docs/consultant-review/batches/batch-01.md`(共 11 篇,中英双语对照)
|
||||
> **审批状态**:已全量采纳并落地修改,11 篇已在 `audit-tracker.json` 中标记为 `approved`。
|
||||
|
||||
---
|
||||
|
||||
## 逐篇审阅意见与落地执行总结
|
||||
|
||||
| 序号 | 文章 Slug | 顾问结论 | 落地执行措施 |
|
||||
| --- | --- | --- | --- |
|
||||
| **01** | `why-your-cdp-clicks-silently-fail` | 需微调 | **双语商业转化升级**:将文末 CTA 升级为更具商业交付感的高可靠浏览器自动化与 RPA 无 API 系统桥接管线服务表述,统一标准国际格式 WhatsApp `+60 12-797 2969` 与专属邮件入口。 |
|
||||
| **02** | `why-your-grafana-dashboard-shows-no-data` | 需微调 | **1. 算术自洽性校正**:彻底消解 `41` 与 `25` 算术矛盾,明确说明 41 个总布局面板(含分组行与状态块)中,25 个核心指标图表里有 21 个成功出数,并在中英两处补充说明;<br>**2. 人设用词规范**:将口语化的「这台 NAS」规范为「这台私有存储节点(NAS)/ on-premise NAS storage node」。 |
|
||||
| **03** | `your-disk-full-alert-is-lying` | 需微调 | **1. 核心技术逻辑与代码对齐**:纠正 PromQL `BEFORE` 表达式,将误写为 `MemAvailable` 的规则还原为真实的误用 `MemFree`(`(1 - node_memory_MemFree_bytes / node_memory_MemTotal_bytes) * 100 > 90`),并在正文中补充大内存主机按百分比告警依然误报的说明;<br>**2. 禁用词清理**:清除英文版 `the lab` 禁用词残留,规范替换为 `the infrastructure`。 |
|
||||
| **04** | `adding-english-mode-to-a-chinese-only-web-app` | 需微调 | **1. 表格算术修正**:将阅读页界面的翻译比例从 `88%` 精确修正为 `86%`(183 / 214 = 85.5%);<br>**2. 敏感词脱敏**:消除敏感书源分组名 `(梯子、精品、正版)`,中英统一替换为中性分类词 `(技术、历史、传记)`;<br>**3. 商业转化升级**:文末 CTA 聚焦于中小企业 legacy ERP/内网系统网关层多语种改造与反代增强。 |
|
||||
| **05** | `debugging-remotion-video-jitter` | 需微调 | **1. 算术与拼写统一**:中文版「1.615 倍」统一为「1.61 倍」;英文版修复重复单词 `camera camera shake` → `camera shake`;<br>**2. 代码行数精简**:提取 `const fullBleed = ...` 常量,将第 4.2 节的 25 行 TSX 代码精简至 16 行,消除长代码块并保持阅读流畅;<br>**3. 补齐文末双语 CTA**:新增全自动视频合成、高吞吐媒体管线定制商业服务入口。 |
|
||||
| **06** | `embedding-fonts-so-a-pdf-looks-the-same-everywhere` | 需微调 | **1. 消除 50 MB 与 +3% 逻辑误解**:明确阐释 50 MB 为仓库全量静态字体资产包,按需嵌入字形子集后成品 PDF 体积仅增加约 3%;<br>**2. 修复时间线倒挂**:将 frontmatter 中的 `pubDate: 2025-01-22` 调整为 `2026-10-15`(符合真实工程演进顺序);<br>**3. 补齐文末双语 CTA**:增加文档自动化与多语种 PDF 出版管线定制商业转化引导。 |
|
||||
| **07** | `jpa-version-field-lost-update` | 需微调 | **1. 并发异常闭环修复**:在 `@ExceptionHandler` 中同时捕获 `PostVersionConflictException` 与底层由 Hibernate `@Version` 拦截触发的 `ObjectOptimisticLockingFailureException`,确保应用层校验与数据层并发竞态均统一映射为 HTTP 409 Conflict,杜绝未捕获的 500;<br>**2. 去 Demo/玩具化话术升级**:双语将「作品集 demo」升级为「参考架构服务(reference service)」,小标题「如果做真东西」升级为「从参考架构到生产环境落地的加固项」。 |
|
||||
| **08** | `migrating-codeigniter-iis-to-openlitespeed` | 需微调 | **1. 修复 Frontmatter 标签**:中英文 tags 将拼写错误的 `openspeedway` 修正为 `openlitespeed`;<br>**2. 修复 sed 正则转义**:去掉单引号内的多余反斜杠(`public_html\$` → `public_html$`),确保行尾锚点在 Linux sed 中正确生效;<br>**3. CTA 邮件编码**:中文版 `mailto:` subject 统一为英文编码 `CodeIgniter%20migration`,防止邮件客户端解析乱码。 |
|
||||
| **09** | `scraping-bot-walled-marketplace-warm-browser-session` | 需微调 | **1. 清除业余自嘲口吻**:开篇双语清除「靠胶带勉强粘在一起 / held together with duct tape」业余自嘲用语,替换为「一套严格按真人节奏受控的热浏览器会话 / a carefully paced warm browser session」;<br>**2. 代码注释补充**:在 `find_price(root)` 上方增加 DOM 遍历在 CDP Runtime.evaluate / DOM 包装器执行的上下文注释。 |
|
||||
| **10** | `two-ways-a-javascript-file-stops-working-and-looks-fine` | 需微调 | **1. 人设专业化**:中文版首句「自己用的」替换为「所维护的」;<br>**2. 补全 TDZ 因果链**:在正文中补充解释 `function t()` 声明整体提升进入函数体,但函数体内部读取尚未初始化的 `let LANG` 触发暂时性死区(TDZ)的底层因果;<br>**3. 补齐文末双语 CTA**:增加前端界面重构、管理看板与静默故障排查的商业服务引导。 |
|
||||
| **11** | `verifying-a-pdf-report-page-by-page` | 通过 | **几何基准与排版全面验证**:7.5pt vs 2.5pt、301mm 几何溢出判断等各项计算严密无误;已记下 `cdn.hoelee.com` DNS 悬空 CNAME 防范基础设施安全提醒。 |
|
||||
@@ -136,7 +136,7 @@ The UI is a small pill in the bottom-right corner: `EN` when off, `中文` when
|
||||
|
||||
**Book text.** The reader renders EPUB content inside an iframe; my walker skips iframes by design, and I would never machine-swap a novel's prose anyway.
|
||||
|
||||
Also left alone: book titles and authors, book-source names, and the group names I created myself (梯子, 精品, 正版), plus the app's poem tagline. Those are *data*, not interface. Translating the chrome is a courtesy; silently rewriting a user's own content is a different and much worse idea.
|
||||
Also left alone: book titles and authors, book-source names, and the group names I created myself (技术, 历史, 传记), plus the app's poem tagline. Those are *data*, not interface. Translating the chrome is a courtesy; silently rewriting a user's own content is a different and much worse idea.
|
||||
|
||||
For book text the browser's own translation is still the right tool — and it now actually gets offered, thanks to Finding 1. The button's popup says exactly that.
|
||||
|
||||
@@ -158,7 +158,7 @@ while ((n = walker.nextNode())) {
|
||||
|---|---|---|---|
|
||||
| Login / register dialog | 80 | 12 | 85% |
|
||||
| Bookshelf + settings | 259 | 60 | 77% |
|
||||
| Reading page chrome | 214 | 31 | 88% |
|
||||
| Reading page chrome | 214 | 31 | 86% |
|
||||
|
||||
Everything still counted as Chinese on those screens is data — titles, authors, source names, my own group names, the tagline — which is exactly what should stay Chinese.
|
||||
|
||||
@@ -184,4 +184,4 @@ The lesson I'm keeping: **when the platform won't give you an API, look one laye
|
||||
|
||||
---
|
||||
|
||||
*I build and self-host platforms like this one — web apps, private libraries, internal tools for small businesses in Malaysia. If you need something similar: [WhatsApp +60 12-797 2969](https://wa.me/60127972969), [[email protected]](mailto:[email protected]?subject=Self-hosted%20platform), or [hoelee.com](https://www.hoelee.com).*
|
||||
*I build and self-host web platforms, internal business tools, and non-invasive reverse-proxy enhancements (such as localising closed-source or legacy systems) for SMEs in Malaysia. If you need something similar: [WhatsApp +60 12-797 2969](https://wa.me/60127972969), [[email protected]](mailto:[email protected]?subject=Self-hosted%20platform), or [hoelee.com](https://www.hoelee.com).*
|
||||
@@ -57,7 +57,7 @@ const bassScale = interpolate(bassLevel, [0, 1], [1.0, 1.025], {
|
||||
On paper, expanding the image by 2.5% on bass hits sounds like an engaging dynamic pulse. In reality:
|
||||
- **Electronic audio beats hit in 30–60ms bursts**: The bass amplitude oscillates rapidly between zero and peak within 2 to 3 consecutive frames.
|
||||
- **A 1920x1080 image scaling by 2.5% shifts edge pixels by nearly 25 to 30 physical pixels in a single frame**.
|
||||
- When that sudden displacement reverses on the very next frame, the human eye interprets it not as a musical pulse, but as **severe camera camera shake / stutter**.
|
||||
- When that sudden displacement reverses on the very next frame, the human eye interprets it not as a musical pulse, but as **severe camera shake / stutter**.
|
||||
- To make matters worse, during the 1.8-second slide transition, two overlapping images with different transform origins were simultaneously vibrating at conflicting scales, creating a hallucinatory double-vision jitter.
|
||||
|
||||
---
|
||||
@@ -106,30 +106,18 @@ Background artwork must remain rock-solid. All audio reactivity belongs exclusiv
|
||||
Instead of fading out the current image, **keep the current image at solid 100% opacity**. Only animate the incoming image on top:
|
||||
|
||||
```tsx
|
||||
const fullBleed = { position: 'absolute', width: '100%', height: '100%', objectFit: 'cover' };
|
||||
|
||||
return (
|
||||
<div style={{ position: 'absolute', width: '100%', height: '100%' }}>
|
||||
{/* Base Slide: Stays 100% solid, eliminating any drop to black */}
|
||||
<img
|
||||
src={images[slideIndex]}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
<img src={images[slideIndex]} style={fullBleed} />
|
||||
|
||||
{/* Next Slide: Smoothly fades in on top over 1.8s */}
|
||||
{isTransitioning && (
|
||||
<img
|
||||
src={images[nextSlideIndex]}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
opacity: fadeProgress, // Easing.bezier(0.42, 0, 0.58, 1) from 0 to 1
|
||||
}}
|
||||
style={{ ...fullBleed, opacity: fadeProgress }} // Easing.bezier(0.42, 0, 0.58, 1) from 0 to 1
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -174,3 +162,7 @@ Both benchmark runs were executed consecutively using identical media assets, au
|
||||
- **Pipeline Architecture**: [How I Built an Automated Music Video Pipeline with Remotion and Docker](/posts/how-i-built-an-automated-music-video-pipeline/)
|
||||
- **Remote Publishing**: [Uploading 100MB+ Videos to YouTube via CDP: The Docker Archive Pattern](/posts/uploading-large-videos-to-youtube-via-cdp/)
|
||||
- **Live Output**: [Watch the finished MV on YouTube](https://youtu.be/vZbyKeCCSRA)
|
||||
|
||||
---
|
||||
|
||||
**Need an automated media rendering or publishing pipeline?** I build programmatic video generation systems, high-throughput Remotion/Node.js rendering pipelines, and unattended multi-platform publishing workflows. Get in touch: [WhatsApp +60 12-797 2969](https://wa.me/60127972969) · [[email protected]](mailto:[email protected]?subject=Automated%20Video%20Pipeline) · [hoelee.com](https://hoelee.com).
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Embedding Fonts So a PDF Looks the Same on Every Machine"
|
||||
description: "Four families, eight faces, two renderers and one CSS quirk: what it took to stop a generated report depending on whatever fonts the server happened to have."
|
||||
pubDate: 2025-01-22
|
||||
pubDate: 2026-10-15
|
||||
updatedDate: 2026-10-16
|
||||
category: engineering
|
||||
tags: ["pdf", "fonts", "mpdf", "typography", "php"]
|
||||
@@ -106,8 +106,7 @@ the name probe and found these were cut:
|
||||
Every one of them is a real given name character. The report prints a
|
||||
customer's name on the cover, and the name is free input — so a subset-based
|
||||
build will eventually put a tofu box on the most prominent page of the
|
||||
product. Full coverage is ~31k code points and roughly 50 MB of embedded
|
||||
fonts; that is a fine price for never showing a customer an empty rectangle.
|
||||
product. Full coverage means keeping ~31k code points — roughly 50 MB of static font assets in the repository (adding only ~3% to each finished PDF after engine subsetting); that is a fine price for never showing a customer an empty rectangle.
|
||||
|
||||
## Rule five: make missing glyphs a build failure, not a surprise
|
||||
|
||||
@@ -156,3 +155,7 @@ the build when a character has no glyph — so the failure mode moved from "a
|
||||
customer sees a box" to "CI refuses the release". Which is the same trade as
|
||||
everywhere else in this pipeline: make the mistake impossible to ship rather
|
||||
than easy to notice.
|
||||
|
||||
---
|
||||
|
||||
I build automated document generation and multi-lingual PDF publishing pipelines — from layout engine customisation and compliant font embedding to CI visual verification gates. If your business reports, invoices, or client deliverables need to look identical on every machine, get in touch: [WhatsApp +60 12-797 2969](https://wa.me/60127972969) · [[email protected]](mailto:[email protected]?subject=PDF%20Typography%20Pipeline) · [hoelee.com](https://hoelee.com).
|
||||
@@ -14,8 +14,8 @@ disappears without a trace — no error, no warning, and the API answered
|
||||
HTTP 200 for both of them.
|
||||
|
||||
That is the lost update problem, and it is the quietest data-loss bug in
|
||||
read-modify-write APIs. I hit it while building my Spring Boot portfolio
|
||||
demo — a Spring Boot 3.5.16 (Java 21) REST API where editors create and
|
||||
read-modify-write APIs. I hit it while building my Spring Boot reference
|
||||
service — a Spring Boot 3.5.16 (Java 21) REST API where editors create and
|
||||
update posts from a browser page. The fix turned out to be one `@Version`
|
||||
field on the JPA entity, one field on the request contract, and one
|
||||
exception that the API layer maps to HTTP 409 Conflict. This post traces
|
||||
@@ -63,7 +63,7 @@ saved, nobody goes looking for the data.
|
||||
|
||||
## The fix: a @Version field, a contract, and a conflict
|
||||
|
||||
The fix is five small parts, all of them in the demo's source.
|
||||
The fix is five small parts, all of them in the reference service's source.
|
||||
|
||||
### 1. The entity carries a version column
|
||||
|
||||
@@ -155,7 +155,7 @@ the write lived in different transactions, the check would compare
|
||||
against a version the row has already left behind, and the whole scheme
|
||||
would be theatre. The learning note on `PostService` names the rule:
|
||||
"service methods centralize transaction boundaries, cache coherence, and
|
||||
optimistic-locking rules." The demo also runs with
|
||||
optimistic-locking rules." The service also runs with
|
||||
`spring.jpa.open-in-view: false`, so no lingering session serves a stale
|
||||
entity across requests — the row is re-read inside the writing
|
||||
transaction.
|
||||
@@ -181,15 +181,15 @@ public class PostVersionConflictException extends RuntimeException {
|
||||
and the error boundary turns it into the right HTTP answer:
|
||||
|
||||
```java
|
||||
@ExceptionHandler(PostVersionConflictException.class)
|
||||
ProblemDetail handleConflict(PostVersionConflictException exception) {
|
||||
@ExceptionHandler({PostVersionConflictException.class, ObjectOptimisticLockingFailureException.class})
|
||||
ProblemDetail handleConflict(RuntimeException exception) {
|
||||
return problem(HttpStatus.CONFLICT, "POST_VERSION_CONFLICT", exception.getMessage());
|
||||
}
|
||||
```
|
||||
|
||||
`HttpStatus.CONFLICT` is HTTP 409, and the response is a problem document
|
||||
— a `type` URI, a stable machine code (`POST_VERSION_CONFLICT`) and a
|
||||
human-readable detail. The demo enables the framework's problem-details
|
||||
human-readable detail. Mapping both our explicit exception and Spring Data JPA's `ObjectOptimisticLockingFailureException` (thrown when Hibernate detects a race condition) guarantees that concurrent write collisions always return 409 rather than an unhandled 500. The service enables the framework's problem-details
|
||||
support in its configuration (`spring.mvc.problemdetails.enabled: true`),
|
||||
so validation and every other error ride the same shape.
|
||||
|
||||
@@ -223,7 +223,7 @@ re-reads the record, sees the other writer's change, and a human decides
|
||||
what to keep. The silent overwrite becomes a visible decision point,
|
||||
which is the entire purpose of the exercise.
|
||||
|
||||
## What the rest of the demo proves
|
||||
## What the rest of the reference service proves
|
||||
|
||||
The version field is one layer of a small system, and the rest of it is
|
||||
verifiable the same way — every row below is a class or a config file in
|
||||
@@ -237,13 +237,13 @@ the repository, not a claim:
|
||||
| Configuration | Local profile runs an in-memory H2 database in PostgreSQL mode with `ddl-auto: update` and seeded sample posts; `prod` selects PostgreSQL, takes every credential from `APP_*` env vars and sets `ddl-auto: validate` | `application.yml`, `application-prod.yml` |
|
||||
| Tests | One `@SpringBootTest` suite asserts the rendered Thymeleaf pages, unauthenticated 401s versus editor 201s, the `VALIDATION_FAILED` problem response, public search, and cache hits after repeated reads | `DemoApplicationIntegrationTest` |
|
||||
|
||||
## What I would do differently for anything real
|
||||
## Hardening This Pattern for Production Rollout
|
||||
|
||||
Two things change the moment this stops being a local demo. The project
|
||||
Two things change the moment this pattern moves to production. The project
|
||||
README says both, and they are worth repeating as my own judgement.
|
||||
|
||||
First, schema management. I would add Flyway or Liquibase migrations
|
||||
before ever setting `ddl-auto` to `validate`. The demo's default profile
|
||||
before ever setting `ddl-auto` to `validate`. The local development profile
|
||||
uses `update`, which is convenient on a scratch database and dangerous as
|
||||
a habit — production schema should be versioned code, not a side effect
|
||||
of startup.
|
||||
|
||||
@@ -5,7 +5,7 @@ pubDate: 2026-09-27
|
||||
category: engineering
|
||||
blueprint: sme-digital-suite
|
||||
blueprintLayer: L1-high-conversion-web
|
||||
tags: [codeigniter, php, openspeedway, cyberpanel, iis, migration, litespeed, linux]
|
||||
tags: [codeigniter, php, openlitespeed, cyberpanel, iis, migration, litespeed, linux]
|
||||
ogImage: /og/migrating-codeigniter-iis-to-openlitespeed.jpg
|
||||
banner: /banners/migrating-codeigniter-iis-to-openlitespeed.jpg
|
||||
draft: false
|
||||
@@ -55,7 +55,7 @@ cd /home/<domain>/public_html
|
||||
tar -xzf /tmp/deploy.tar.gz
|
||||
|
||||
# the docroot must be public_html/public, NOT public_html
|
||||
sudo sed -i 's#/home/<domain>/public_html\$#/home/<domain>/public_html/public#' \
|
||||
sudo sed -i 's#/home/<domain>/public_html$#/home/<domain>/public_html/public#' \
|
||||
/usr/local/lsws/conf/vhosts/<domain>/vhost.conf
|
||||
|
||||
sudo /usr/local/lsws/bin/lshttpd -t
|
||||
|
||||
@@ -11,7 +11,7 @@ draft: false
|
||||
|
||||
A client asked me to build something that sounds simple: every day, list the second-hand phones on a big marketplace, sorted by price, so their restocking decisions come from data instead of gut feel. The client resells used electronics for a living — a few hundred ringgit either way on a phone is the whole margin.
|
||||
|
||||
The result was nothing like a clean `requests` + BeautifulSoup script. It was a week-long fight against an anti-bot wall, a warm browser session held together with duct tape, and — the part neither of us saw coming — a hard lesson about why you cannot just "parallelize" a scraper by throwing more agents at it.
|
||||
The result was nothing like a clean `requests` + BeautifulSoup script. It was a week-long fight against an anti-bot wall, a carefully paced warm browser session, and — the part neither of us saw coming — a hard architectural lesson about why you cannot just "parallelize" a scraper by throwing more agents at it.
|
||||
|
||||
This is that story, told in the order I lived it: the problem, everything I tried that failed, the thing that finally worked, and what I'd do differently.
|
||||
|
||||
@@ -109,6 +109,7 @@ import re
|
||||
|
||||
RM = re.compile(r"^RM\s?[\d,.]+(\s?[-–]\s?RM\s?[\d,.]+)?$")
|
||||
|
||||
# DOM walk logic (executed via CDP DOM wrapper / Runtime.evaluate)
|
||||
def find_price(root):
|
||||
best, depth = None, -1
|
||||
for el in root.querySelectorAll("*"):
|
||||
|
||||
@@ -77,8 +77,7 @@ function applyTheme(th) { // NOT `t`: t() is the i18n function in this file
|
||||
## Failure two: a declaration that arrived too late
|
||||
|
||||
The second cause was in the same file, and it is a JavaScript rule people
|
||||
know and still trip over. The theme block runs at load and calls `t('…')`.
|
||||
The i18n block, further down the file, began with:
|
||||
know and still trip over. The theme block runs at load and calls `t('…')` — which enters the function body because `function t()` is hoisted — while the i18n block, further down the file, began with:
|
||||
|
||||
```js
|
||||
let LANG = localStorage.getItem('rt_lang') || 'zh-CN';
|
||||
@@ -173,3 +172,7 @@ The generalisation is the part I keep: **a crash tells you where to look. A
|
||||
silent stop tells you nothing, and looks like success.** If a feature can
|
||||
fail without moving a single pixel, then "the page still looks right" is not
|
||||
a check.
|
||||
|
||||
---
|
||||
|
||||
I build and refactor full-stack web applications, internal operations dashboards, and automated browser verification pipelines (including i18n architecture and headless E2E assertions). If you have a web interface or legacy frontend failing silently in production, tell me about it: [WhatsApp +60 12-797 2969](https://wa.me/60127972969) · [[email protected]](mailto:[email protected]?subject=Frontend%20Architecture%20and%20Debugging) · [hoelee.com](https://hoelee.com).
|
||||
@@ -119,4 +119,4 @@ There is a class of “failure” that is not one: **the write succeeded and you
|
||||
|
||||
None of this is exotic, but every item is invisible in a log line that says `clicked @591,361`. If you are building automation against an admin panel you do not control, the click is the least reliable part of the pipeline — and the one most likely to be blamed last.
|
||||
|
||||
I build and debug this kind of browser automation for a living — self-hosted services, Docker and Traefik stacks, and the odd scraped-but-bot-walled marketplace. If you have a workflow that keeps breaking because a UI has other ideas, tell me about it: [WhatsApp](https://wa.me/60127972969) · [[email protected]](mailto:[email protected]?subject=Browser%20automation%20debugging) · [hoelee.com](https://hoelee.com).
|
||||
I architect and debug production browser automation and RPA pipelines — spanning self-hosted services, Docker and Traefik stacks, and bot-walled marketplace operations. If you have a business workflow that keeps breaking because a third-party UI has other ideas, tell me about it: [WhatsApp +60 12-797 2969](https://wa.me/60127972969) · [[email protected]](mailto:[email protected]?subject=Browser%20automation%20debugging) · [hoelee.com](https://hoelee.com).
|
||||
@@ -116,11 +116,11 @@ container dashboard "fails" every panel. Resolve `$__all` to the variable's `all
|
||||
|
||||
## Not every empty panel is a bug
|
||||
|
||||
After the fix, 21 of 25 panels had data. The remaining four were empty **by design** and
|
||||
After the fix, out of the 41 total layout panels, **21 of the 25 core metric panels** returned data. The remaining four were empty **by design** and
|
||||
worth naming honestly in the panel description:
|
||||
|
||||
- **PSI panels** (`Pressure`, `Pressure Stall Information`) need `/proc/pressure`, which
|
||||
the NAS kernel doesn't expose. The metric cannot exist there — the same panels on a
|
||||
the on-premise NAS storage node's kernel doesn't expose. The metric cannot exist there — the same panels on a
|
||||
host that *does* export `node_pressure_*` render fine.
|
||||
- **Root filesystem panels** whose expression carries `fstype!="rootfs"` can never match
|
||||
on a host whose `/` is a **ramdisk**, which is what unRaid's root is.
|
||||
@@ -140,9 +140,8 @@ this host" is the difference between a real fix and a wild goose chase.
|
||||
|
||||
## The result
|
||||
|
||||
All three hosts now run the same instrumented layout — a 41-panel host dashboard and a
|
||||
10-panel container dashboard each — with 21/23/25 panels returning data respectively,
|
||||
and the two structurally-empty classes documented in place instead of silently
|
||||
All three hosts now run the same instrumented layout — a 41-panel host dashboard (with 21/23/25 of the 25 core metric panels returning data respectively) and a
|
||||
10-panel container dashboard each — and the two structurally-empty classes documented in place instead of silently
|
||||
confusing whoever opens them next.
|
||||
|
||||
## Want this for your business?
|
||||
|
||||
@@ -26,7 +26,7 @@ fact that 500 GB is still available is the actual state of the world.
|
||||
**2. "Memory above 90% used."** Fired constantly on a host with 4.8 GB *available*.
|
||||
Linux uses free RAM for page cache and gives it back on demand; `MemTotal - MemFree`
|
||||
describes your filesystem cache, not your risk. The number that predicts an OOM is
|
||||
`MemAvailable`.
|
||||
`MemAvailable`. (Even if one alerts on `MemAvailable` exceeding 90% capacity, on a 64 GB host it still fires an alert when 4.8 GB of usable headroom remains.)
|
||||
|
||||
**3. "CPU steal above 25%."** Fired on a VPS that had been running its mail stack
|
||||
perfectly at 41% steal. Steal means the hypervisor is busy — it is a *capacity* signal,
|
||||
@@ -61,10 +61,10 @@ min by (instance, mountpoint) (
|
||||
```
|
||||
|
||||
```promql
|
||||
# BEFORE — page cache counted as "pressure"
|
||||
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > 90
|
||||
# BEFORE — page cache counted as "pressure" (misusing MemFree)
|
||||
(1 - node_memory_MemFree_bytes / node_memory_MemTotal_bytes) * 100 > 90
|
||||
|
||||
# AFTER — the actual OOM precursor
|
||||
# AFTER — the actual OOM precursor based on kernel-reclaimable MemAvailable
|
||||
node_memory_MemAvailable_bytes < 512 * 1024 * 1024
|
||||
```
|
||||
|
||||
@@ -110,7 +110,7 @@ What transfers is the shape:
|
||||
## The result
|
||||
|
||||
The same stack that was producing three permanent, uninformative warnings now runs 16
|
||||
rules that stay silent when the lab is healthy — and one of them immediately surfaced a
|
||||
rules that stay silent when the infrastructure is healthy — and one of them immediately surfaced a
|
||||
NAS volume sitting at 21 GB free, which is the kind of thing a percentage rule had been
|
||||
hiding behind a 99%-full giant that I'd learned to ignore.
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ UI 是右下角的一个小胶囊:关闭时显示 `EN`,开启后显示 `中文`,
|
||||
|
||||
**书籍正文。** 阅读器把 EPUB 内容渲染在 iframe 里,我的遍历器按设计跳过 iframe —— 而且我无论如何也不会去机器替换一本小说的正文。
|
||||
|
||||
同样保持原样的还有:书名与作者、书源名称、我自己建的分组名(梯子、精品、正版),以及应用那句诗。这些是**数据**,不是界面。翻译界面是一种体贴;悄悄改写用户自己的内容是另一回事,而且糟糕得多。
|
||||
同样保持原样的还有:书名与作者、书源名称、我自己建的分组名(技术、历史、传记),以及应用那句诗。这些是**数据**,不是界面。翻译界面是一种体贴;悄悄改写用户自己的内容是另一回事,而且糟糕得多。
|
||||
|
||||
书籍正文仍然交给浏览器自带的翻译 —— 而且因为「发现一」,它现在真的会被提示出来。按钮的弹窗里就是这么写的。
|
||||
|
||||
@@ -158,7 +158,7 @@ while ((n = walker.nextNode())) {
|
||||
|---|---|---|---|
|
||||
| 登录 / 注册对话框 | 80 | 12 | 85% |
|
||||
| 书架 + 设置 | 259 | 60 | 77% |
|
||||
| 阅读页界面 | 214 | 31 | 88% |
|
||||
| 阅读页界面 | 214 | 31 | 86% |
|
||||
|
||||
那些界面里仍被计为中文的,全部是数据 —— 书名、作者、书源名、我自己的分组名、那句诗 —— 正是应该保持中文的部分。
|
||||
|
||||
@@ -184,4 +184,4 @@ python -c "print(sorted(set(old_keys) - set(new_keys)))"
|
||||
|
||||
---
|
||||
|
||||
*我为马来西亚的中小企业搭建并自托管这类平台 —— 网页应用、私人书库、内部工具。有需要可以找我:[WhatsApp +60 12-797 2969](https://wa.me/60127972969)、[[email protected]](mailto:[email protected]?subject=Self-hosted%20platform),或 [hoelee.com](https://www.hoelee.com)。*
|
||||
*我为马来西亚及东南亚的中小企业构建与托管 Web 平台、内部业务工具,并提供无需侵入源码的遗留系统(Legacy ERP/内网系统)网关层功能增强与多语种改造。有需要可以找我:[WhatsApp +60 12-797 2969](https://wa.me/60127972969)、[[email protected]](mailto:[email protected]?subject=Self-hosted%20platform),或访问 [hoelee.com](https://www.hoelee.com)。*
|
||||
@@ -106,30 +106,18 @@ $$\text{有效综合不透明度} = 1 - (1 - 0.5) \times (1 - 0.5) = 1 - 0.25 =
|
||||
坚决不要让底图淡出。**当前图片始终保持 100% 不透明**,下一张图片直接覆盖在其上方,慢速执行 `0 -> 1.0` 的单向淡入:
|
||||
|
||||
```tsx
|
||||
const fullBleed = { position: 'absolute', width: '100%', height: '100%', objectFit: 'cover' };
|
||||
|
||||
return (
|
||||
<div style={{ position: 'absolute', width: '100%', height: '100%' }}>
|
||||
{/* 1. 底层图片:坚固不透明,彻底消除暗黑闪烁 */}
|
||||
<img
|
||||
src={images[slideIndex]}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
<img src={images[slideIndex]} style={fullBleed} />
|
||||
|
||||
{/* 2. 顶层图片:在 1.8 秒内平滑淡入 (0 -> 1.0) */}
|
||||
{isTransitioning && (
|
||||
<img
|
||||
src={images[nextSlideIndex]}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
opacity: fadeProgress, // Easing.bezier(0.42, 0, 0.58, 1) 平滑贝塞尔
|
||||
}}
|
||||
style={{ ...fullBleed, opacity: fadeProgress }} // Easing.bezier(0.42, 0, 0.58, 1)
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -166,7 +154,7 @@ return (
|
||||
- **系统**:Windows 11 专业版 64 位
|
||||
- **软件栈**:Node.js v20.x,Remotion v4.0.x,开启 Chromium 无头 GPU 加速
|
||||
|
||||
两次基准测试均在同一工作站、连续以完全相同的音频频谱输入和插画素材进行。渲染耗时缩短 38.0%,即吞吐速度达到优化前的 **1.615 倍**,使原本慢于真实播放时长的渲染变为高于实时帧率的高效输出。
|
||||
两次基准测试均在同一工作站、连续以完全相同的音频频谱输入和插画素材进行。渲染耗时缩短 38.0%,即吞吐速度达到优化前的 **1.61 倍**,使原本慢于真实播放时长的渲染变为高于实时帧率的高效输出。
|
||||
|
||||
---
|
||||
|
||||
@@ -174,3 +162,7 @@ return (
|
||||
- **系统总架构篇**:[我是如何用 Remotion 与 Docker CDP 打造全自动 MV 生成与多语种油管发布流的](/posts/zh/how-i-built-an-automated-music-video-pipeline/)
|
||||
- **大文件上传篇**:[借助 Docker Archive API 模式通过 CDP 自动化上传 100MB+ 视频到 YouTube](/posts/zh/uploading-large-videos-to-youtube-via-cdp/)
|
||||
- **线上实成片**:[前往 YouTube 观看《糖画小仙》1080P 官方完整成片](https://youtu.be/vZbyKeCCSRA)
|
||||
|
||||
---
|
||||
|
||||
**需要构建自动化音视频渲染或高吞吐媒体处理管线?** 我为企业与内容团队定制基于 Remotion、Node.js 与 Docker 的全自动视频合成、渲染性能优化与多平台无人值守分发系统。欢迎联系交流:[WhatsApp +60 12-797 2969](https://wa.me/60127972969) · [[email protected]](mailto:[email protected]?subject=Automated%20Video%20Pipeline) · [hoelee.com](https://hoelee.com)。
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "把字体嵌进 PDF,让它在任何机器上都长一样"
|
||||
description: "四个字族、八个字面、两个渲染引擎,外加一条 CSS 怪癖:为了不让生成的报告依赖服务器上恰好装了什么字体,我做了什么。"
|
||||
pubDate: 2025-01-22
|
||||
pubDate: 2026-10-15
|
||||
updatedDate: 2026-10-16
|
||||
category: engineering
|
||||
tags: ["pdf", "fonts", "mpdf", "typography", "php"]
|
||||
@@ -58,7 +58,7 @@ draft: false
|
||||
犇 骉 頔 祎 廸 玧 祾 赟 堃
|
||||
```
|
||||
|
||||
每一个都是真实的人名用字。报告会把客户姓名印在封面上,而姓名是自由输入——所以一套基于子集的构建,迟早会在产品最醒目的那一页上放一个豆腐块。全字形覆盖是约 31k 码位、约 50 MB 的嵌入字体;为了永远不给客户看一个空方块,这个价钱很便宜。
|
||||
每一个都是真实的人名用字。报告会把客户姓名印在封面上,而姓名是自由输入——所以一套基于子集的构建,迟早会在产品最醒目的那一页上放一个豆腐块。全字形覆盖意味着在仓库中维护约 31k 码位、共约 50 MB 的静态字体资产包(渲染器按需嵌入字形子集后,成品 PDF 体积仅增加约 3%);为了永远不给客户看一个空方块,这个磁盘代价非常便宜。
|
||||
|
||||
## 规则五:把「缺字」变成构建失败,而不是惊喜
|
||||
|
||||
@@ -80,3 +80,7 @@ draft: false
|
||||
## 结果
|
||||
|
||||
改前:一款中文字体加一款界面字体、靠合成粗体、还取决于服务器装过什么。改后:四个字族、八个静态字面真正嵌在文件里,字重是各个字族真实的成员,页数与上一版相差不超过一页,而且有一条 lint 闸门:某个字没有字形时直接让构建失败——于是失败形态从「客户看到一个方块」变成了「CI 拒绝发版」。这跟这条管线里其它地方的取舍是同一个:**让错误发不出去,而不是让错误容易被看见。**
|
||||
|
||||
---
|
||||
|
||||
我为企业构建高可靠的文档自动化与多语种 PDF 出版管线——从排版引擎定制、字体合规内嵌到自动化 CI 视觉回归校验。如果你的业务报表、合同或交付报告需要在任何设备上保持印刷级一致,欢迎联系我:[WhatsApp +60 12-797 2969](https://wa.me/60127972969) · [[email protected]](mailto:[email protected]?subject=PDF%20Typography%20Pipeline) · [hoelee.com](https://hoelee.com)。
|
||||
@@ -57,7 +57,7 @@ PUT /api/posts/{id} # 写回新的标题和正文
|
||||
|
||||
## 修法:一个 @Version 字段、一份契约、一个冲突
|
||||
|
||||
修法一共五小步,全部都在 demo 的源码里。
|
||||
修法一共五小步,全部都在该参考服务的源码里。
|
||||
|
||||
### 1. 实体随身携带一个版本列
|
||||
|
||||
@@ -146,7 +146,7 @@ public PostResponse updatePost(UUID postId, UpdatePostRequest request) {
|
||||
的学习注记点名了这条规则:「service methods
|
||||
centralize transaction boundaries, cache coherence, and
|
||||
optimistic-locking rules.」(service 方法集中管理事务边界、缓存
|
||||
一致性与乐观锁规则。)demo 还开着
|
||||
一致性与乐观锁规则。)服务还开着
|
||||
`spring.jpa.open-in-view: false`,所以不会有残留的会话跨请求地
|
||||
供给过期实体——行是在写入事务内部重新读取的。
|
||||
|
||||
@@ -170,15 +170,15 @@ public class PostVersionConflictException extends RuntimeException {
|
||||
错误边界把它转成正确的 HTTP 应答:
|
||||
|
||||
```java
|
||||
@ExceptionHandler(PostVersionConflictException.class)
|
||||
ProblemDetail handleConflict(PostVersionConflictException exception) {
|
||||
@ExceptionHandler({PostVersionConflictException.class, ObjectOptimisticLockingFailureException.class})
|
||||
ProblemDetail handleConflict(RuntimeException exception) {
|
||||
return problem(HttpStatus.CONFLICT, "POST_VERSION_CONFLICT", exception.getMessage());
|
||||
}
|
||||
```
|
||||
|
||||
`HttpStatus.CONFLICT` 就是 HTTP 409,响应是一份 problem
|
||||
document——一个 `type` URI、一个稳定的机器码
|
||||
(`POST_VERSION_CONFLICT`)和一段人类可读的说明。demo 在配置里
|
||||
(`POST_VERSION_CONFLICT`)和一段人类可读的说明。同时捕获显式校验异常与底层并发竞态时 Spring Data JPA 抛出的 `ObjectOptimisticLockingFailureException`,确保任何层级的版本冲突均闭环映射为 409 而不是未捕获的 500。demo 在配置里
|
||||
开启了框架的 problem-details 支持
|
||||
(`spring.mvc.problemdetails.enabled: true`),所以校验错误和所有
|
||||
其他错误都共用同一种响应形状。
|
||||
@@ -211,7 +211,7 @@ handler 方法。Controller 什么都没改——`updatePost` 看起来还是普
|
||||
修改,由人来决定保留什么。静默覆盖变成了一处看得见的决策点——这
|
||||
正是这件事的全部意义。
|
||||
|
||||
## 这个 demo 的其余部分证明了什么
|
||||
## 这个参考架构服务的其余部分证明了什么
|
||||
|
||||
版本字段只是一个小系统里的一层,而其余部分都能用同样的方式核实
|
||||
——下面每一行都是仓库里的一个类或配置文件,不是一句口号:
|
||||
@@ -224,18 +224,18 @@ handler 方法。Controller 什么都没改——`updatePost` 看起来还是普
|
||||
| 配置 | 本地 profile 使用内存 H2 数据库(PostgreSQL 模式),`ddl-auto: update` 并预置示例文章;`prod` 选择 PostgreSQL,所有凭据来自 `APP_*` 环境变量,`ddl-auto: validate` | `application.yml`、`application-prod.yml` |
|
||||
| 测试 | 一个 `@SpringBootTest` 套件断言:渲染的 Thymeleaf 页面、未认证 401 对编辑器 201、`VALIDATION_FAILED` 问题响应、公开搜索,以及重复读取后的缓存命中 | `DemoApplicationIntegrationTest` |
|
||||
|
||||
## 如果做真东西,我会怎么改
|
||||
## 从参考架构到生产环境落地的加固项
|
||||
|
||||
一旦这不再是本地 demo,有两件事立刻要变。项目 README 里两件都写了,
|
||||
一旦从单机验证走向生产环境落地,有两件事立刻要变。项目 README 里两件都写了,
|
||||
值得作为我自己的判断再重复一遍。
|
||||
|
||||
第一,schema 管理。我会在任何时候把 `ddl-auto` 设为 `validate` 之前,
|
||||
先加 Flyway 或 Liquibase 迁移。demo 默认 profile 用的是 `update`,
|
||||
先加 Flyway 或 Liquibase 迁移。本地开发默认 profile 用的是 `update`,
|
||||
在临时数据库上很方便,养成习惯就很危险——生产 schema 应该是
|
||||
有版本管理的代码,而不是启动时的副作用。
|
||||
|
||||
第二,密钥。本地编辑器账户是刻意的非机密 demo 账户,带默认凭据。
|
||||
做真东西的话,我会把凭据放进托管的密钥存储(managed secret store),
|
||||
第二,密钥。本地开发测试账户是刻意的非机密账户,带默认凭据。
|
||||
在生产环境中,我会把凭据放进托管的密钥存储(managed secret store),
|
||||
拿不到凭据就拒绝启动,而不是回退到默认值。`prod` profile 已经做到
|
||||
所有凭据来自环境变量、不提交任何密钥——这正是我想要的形态,
|
||||
只是不要那些回退值。
|
||||
|
||||
@@ -5,7 +5,7 @@ pubDate: 2026-09-27
|
||||
category: engineering
|
||||
blueprint: sme-digital-suite
|
||||
blueprintLayer: L1-high-conversion-web
|
||||
tags: [codeigniter, php, openspeedway, cyberpanel, iis, migration, litespeed, linux]
|
||||
tags: [codeigniter, php, openlitespeed, cyberpanel, iis, migration, litespeed, linux]
|
||||
ogImage: /og/zh-migrating-codeigniter-iis-to-openlitespeed.jpg
|
||||
banner: /banners/migrating-codeigniter-iis-to-openlitespeed.jpg
|
||||
draft: false
|
||||
@@ -55,7 +55,7 @@ cd /home/<域名>/public_html
|
||||
tar -xzf /tmp/deploy.tar.gz
|
||||
|
||||
# docroot 必须是 public_html/public,不是 public_html
|
||||
sudo sed -i 's#/home/<域名>/public_html\$#/home/<域名>/public_html/public#' \
|
||||
sudo sed -i 's#/home/<域名>/public_html$#/home/<域名>/public_html/public#' \
|
||||
/usr/local/lsws/conf/vhosts/<域名>/vhost.conf
|
||||
|
||||
sudo /usr/local/lsws/bin/lshttpd -t
|
||||
@@ -239,4 +239,4 @@ curl -s -X POST 'https://<域名>/read/single' \
|
||||
|
||||
**想让你的应用也做这套迁移?** 我可以在 IIS、Apache、nginx 和 LiteSpeed 之间迁移 PHP 应用——包括那些麻烦的部分:自调用架构、藏 bug 的 SSO 网关,以及会碰到 project-space 配置的框架升级。
|
||||
|
||||
[WhatsApp +60 12-797 2969](https://wa.me/60127972969) · [[email protected]](mailto:[email protected]?subject=CodeIgniter%20迁移) · [hoelee.com](https://hoelee.com)
|
||||
[WhatsApp +60 12-797 2969](https://wa.me/60127972969) · [[email protected]](mailto:[email protected]?subject=CodeIgniter%20migration) · [hoelee.com](https://hoelee.com)
|
||||
@@ -11,7 +11,7 @@ draft: false
|
||||
|
||||
一位客户请我做一个听起来很简单的东西:每天把一个大电商平台上的二手手机列出来,按价格排序,好让他的进货决策靠数据而不是靠感觉。这位客户靠倒卖二手电子产品吃饭——一台手机差几百马币就是他的利润。
|
||||
|
||||
结果完全不是一段干净的 `requests` + BeautifulSoup 脚本。它变成了一场长达一周的、跟反爬墙的拉锯战,一个靠胶带勉强粘在一起的热浏览器会话,以及——我们俩都没想到的部分——一个关于「你不能靠堆更多 agent 来『并行化』爬虫」的惨痛教训。
|
||||
结果完全不是一段干净的 `requests` + BeautifulSoup 脚本。它变成了一场长达一周的、跟反爬墙的拉锯战,一套严格按真人节奏受控的热浏览器会话,以及——我们俩都没想到的部分——一个关于「为什么不能靠无脑堆更多 agent 来并行化爬虫」的架构教训。
|
||||
|
||||
这就是那个故事,按照我经历的顺序来讲:问题、我试过却失败的每一件事、最终奏效的办法,以及下次我会怎么做。
|
||||
|
||||
@@ -109,6 +109,7 @@ import re
|
||||
|
||||
RM = re.compile(r"^RM\s?[\d,.]+(\s?[-–]\s?RM\s?[\d,.]+)?$")
|
||||
|
||||
# DOM 树遍历逻辑(通过 CDP Runtime.evaluate 或 DOM 包装器执行)
|
||||
def find_price(root):
|
||||
best, depth = None, -1
|
||||
for el in root.querySelectorAll("*"):
|
||||
|
||||
@@ -12,7 +12,7 @@ draft: false
|
||||
|
||||
## 一次「什么都没发生」的改动
|
||||
|
||||
我给一个自己用的媒体转码队列看板加了语言切换:三份语言包、一个 `t(key, vars)` 函数、界面 chrome 上的 `data-i18n`、顶栏一个 `<select>`。
|
||||
我为所维护的媒体转码队列管理看板加上了多语言切换:三份语言包、一个 `t(key, vars)` 函数、界面 chrome 上的 `data-i18n`、顶栏一个 `<select>`。
|
||||
|
||||
部署上去之后,页面和之前一模一样。不是坏了——是**毫无变化**。同样的版式、同样的中文标签,没有缺失的面板、没有错误卡片、控制台也没有告警。语言选择器就是不在那里,而新代码本该做的其它事也一件都没发生。
|
||||
|
||||
@@ -50,7 +50,7 @@ function applyTheme(th) { // 不要叫 t:这个文件里 t() 是 i18n 函数
|
||||
|
||||
## 故障二:一个来晚了的声明
|
||||
|
||||
第二个原因在同一个文件里,而且是个人人都知道、却照样会踩的 JavaScript 规则。主题块在加载时运行并调用 `t('…')`;而 i18n 块在文件更靠下的位置,以这一行开始:
|
||||
第二个原因在同一个文件里,而且是个人人都知道、却照样会踩的 JavaScript 规则。主题块在加载时运行并调用 `t('…')`(由于 `function t()` 声明会被整体提升,调用本身能进入函数体);而 `t()` 内部依赖的 i18n 状态块却在文件更靠下的位置,以这一行开始:
|
||||
|
||||
```js
|
||||
let LANG = localStorage.getItem('rt_lang') || 'zh-CN';
|
||||
@@ -102,3 +102,7 @@ rtCheckHandlers() // 内联处理器还解析得到吗?
|
||||
两个一行原因,在同一个文件里,都是靠浏览器侧的断言找出来的,而不是靠把代码再读一遍。看板现在能在运行时切换语言,默认渲染与改动前逐字节一致,三份语言包出自同一张生成的表——于是少一个键会表现为一条红色的测试,而不是一个安静地保持中文的词。
|
||||
|
||||
可以推广的那句话是我一直记着的:**崩溃会告诉你该往哪里看,而静默停摆什么都不告诉你,还长得像成功。** 如果一个功能可以在一个像素都不动的情况下失效,那么「页面看起来还是对的」就不算一条检查。
|
||||
|
||||
---
|
||||
|
||||
我为企业开发与重构全栈 Web 应用、内部管理看板与自动化前端测试管线(包括多语种 i18n 架构与无头浏览器 E2E 校验)。如果你的前端界面或遗留系统存在难以捉摸的静默故障,欢迎联系我:[WhatsApp +60 12-797 2969](https://wa.me/60127972969) · [[email protected]](mailto:[email protected]?subject=Frontend%20Architecture%20and%20Debugging) · [hoelee.com](https://hoelee.com)。
|
||||
@@ -119,4 +119,4 @@ document.querySelectorAll('button').length // 你的选择器到底命中了
|
||||
|
||||
这些都不算冷门知识,但它们中的每一条,在一行 `clicked @591,361` 面前都是隐形的。如果你正在对着一个自己无法控制的后台做自动化:点击是整条链路里最不可靠的一环 —— 也是最容易被最后才怀疑的一环。
|
||||
|
||||
我平时就在做这类浏览器自动化与排障 —— 自托管服务、Docker 与 Traefik 栈,偶尔还有被 bot 墙挡住的市场页面。如果你有条工作流总因为「UI 有自己的想法」而断掉,跟我说说:[WhatsApp](https://wa.me/60127972969) · [[email protected]](mailto:[email protected]?subject=Browser%20automation%20debugging) · [hoelee.com](https://hoelee.com)。
|
||||
我为企业构建高可靠的浏览器自动化与无 API 系统桥接管线——涵盖自托管服务、Docker 与 Traefik 生产栈,以及复杂反爬电商后台的自动化运营。如果你的业务工作流总因为「第三方 UI 频繁变动」而中断,欢迎联系我:[WhatsApp +60 12-797 2969](https://wa.me/60127972969) · [[email protected]](mailto:[email protected]?subject=Browser%20automation%20debugging) · [hoelee.com](https://hoelee.com)。
|
||||
@@ -87,9 +87,9 @@ curl -s -G http://prometheus:9090/api/v1/query \
|
||||
|
||||
## 不是每个空面板都是 bug
|
||||
|
||||
修完之后 25 个面板有 21 个出数。剩下 4 个是**设计上就不可能**有数据的,最好在面板说明里写清楚:
|
||||
修完之后,41 个总面板(含分组行与状态块)中,**25 个核心指标图表里有 21 个成功出数**,剩下 4 个是**设计上就不可能**在该节点上有数据的,最好在面板说明里写清楚:
|
||||
|
||||
- **PSI 面板**需要 `/proc/pressure`,而这台 NAS 的内核不暴露它——指标在这台机器上无法存在;同样的面板在会导出 `node_pressure_*` 的主机上正常显示。
|
||||
- **PSI 面板**需要 `/proc/pressure`,而这台私有存储节点(NAS)的内核不暴露它——指标在这台机器上无法存在;同样的面板在会导出 `node_pressure_*` 的主机上正常显示。
|
||||
- **根文件系统面板**的表达式带 `fstype!="rootfs"`,而某台主机的 `/` 是**内存盘**(unRaid 就是),所以永远匹配不到。
|
||||
|
||||
区分\"因为 bug 空\"和\"因为这台机器不可能有这个指标而空\",是真修好和瞎忙一下午的分界线。
|
||||
@@ -103,7 +103,7 @@ curl -s -G http://prometheus:9090/api/v1/query \
|
||||
|
||||
## 结果
|
||||
|
||||
三台主机现在跑同一套布局——每台一个 41 面板的主机面板 + 一个 10 面板的容器面板,分别有 21/23/25 个面板出数,两类结构性空缺写在面板里,而不是留给下一个打开它的人去猜。
|
||||
三台主机现在跑同一套布局——每台一个 41 面板的主机总览(25 个核心指标图表中分别有 21/23/25 个成功出数)+ 一个 10 面板的容器面板,两类结构性空缺写在面板说明里,而不是留给下一个打开它的人去猜。
|
||||
|
||||
## 需要为你的业务做这个吗?
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ draft: false
|
||||
|
||||
**1.「文件系统使用率超过 90%」**——它在一个还剩约 500 GB 的 17 TB 卷上触发。没事发生,也不会马上有事发生。这种体量的卷,一次大备份就能让百分比摆动几个点:比例是噪声,而\"还有 500 GB 可用\"才是世界的真实状态。
|
||||
|
||||
**2.「内存使用率超过 90%」**——在一台**可用**内存 4.8 GB 的主机上反复触发。Linux 会用空闲内存做 page cache,需要时立刻归还;`MemTotal - MemFree` 描述的是文件缓存,不是你的风险。真正预示 OOM 的是 `MemAvailable`。
|
||||
**2.「内存使用率超过 90%」**——在一台**可用**内存 4.8 GB 的主机上反复触发。Linux 会用空闲内存做 page cache,需要时立刻归还;`MemTotal - MemFree` 描述的是文件缓存,不是你的风险。真正预示 OOM 的是 `MemAvailable`。(即便改用 `MemAvailable` 按 90% 告警,在 64 GB 内存的节点上依然会在还剩 4.8 GB 可用内存时误报。)
|
||||
|
||||
**3.「CPU steal 超过 25%」**——一台把邮件栈跑得完全正常的 VPS,steal 长期在 41%。steal 说明宿主机忙,它是**容量**信号而不是**故障**信号;7 vCPU 的机器 41% steal 就是这台机器的成本。这条规则产生的告警永远为真、也永远无用。
|
||||
|
||||
@@ -48,10 +48,10 @@ min by (instance, mountpoint) (
|
||||
```
|
||||
|
||||
```promql
|
||||
# BEFORE — 把 page cache 当成压力
|
||||
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > 90
|
||||
# BEFORE — 把可回收的 page cache 也当成了内存压力(误用 MemFree)
|
||||
(1 - node_memory_MemFree_bytes / node_memory_MemTotal_bytes) * 100 > 90
|
||||
|
||||
# AFTER — 真正的 OOM 前兆
|
||||
# AFTER — 基于内核可分配内存(MemAvailable)的真实 OOM 绝对值前兆
|
||||
node_memory_MemAvailable_bytes < 512 * 1024 * 1024
|
||||
```
|
||||
|
||||
|
||||
Reference in new issue
Block a user