diff --git a/src/content/posts/the-dialog-opened-in-7-seconds-my-script-waited-5.md b/src/content/posts/the-dialog-opened-in-7-seconds-my-script-waited-5.md new file mode 100644 index 0000000..06207db --- /dev/null +++ b/src/content/posts/the-dialog-opened-in-7-seconds-my-script-waited-5.md @@ -0,0 +1,166 @@ +--- +title: "The Dialog Opened in 7.2 Seconds. My Script Waited 5." +description: "A cron job kept reporting 'the composer dialog did not open'. The three DOM stages mounted at 1.8 s, 2.8 s and 7.2 s — and the script only looked once, at 5 s." +pubDate: 2026-10-15 +category: ai +tags: ["automation", "linkedin", "cron", "browser", "agents"] +ogImage: /og/the-dialog-opened-in-7-seconds-my-script-waited-5.png +banner: /banners/the-dialog-opened-in-7-seconds-my-script-waited-5.png +draft: true +--- + +## The failure message that was technically true and completely wrong + +My scheduled posting job reports to my phone when it can't do its job, and +that morning it said: + +```text +⚠️ Cron 'linkedin-daily-post' failed: Script exited with code 1 + LinkedIn daily post — FAILED + FAILED: the composer dialog did not open +``` + +Confident, specific, and useless. Nothing had been published, the site was +up, the browser profile was logged in, and the message told me which +component to blame: a dialog that never appeared. + +The dialog had appeared. It appeared 3.2 seconds before my script decided it +hadn't. + +## Where I looked first, and why that was the wrong place + +An automation that fails against a website has three usual suspects: the +session expired, the site changed its markup, or the account got +rate-limited. I checked the session first — it was healthy. And in my own +project notes, the two earlier failures, four and five days before, carried +the **same** message, with the working theory written down as "the session +may be dead". + +That sentence is the tell. Two identical failures, one theory, and the +theory was never confirmed. A repeated failure with an unconfirmed cause is +a measurement problem, not a diagnosis problem. + +## Measure the mount instead of the message + +So I opened the composer with the browser in the state the script leaves it, +and timed when each piece of the DOM actually arrives: + +```text ++1.8 s the composer dialog element exists ++2.8 s the Post / Proofread buttons exist ++7.2 s the text editor exists <- the node the script actually types into +``` + +The script clicked "start a post", slept five seconds, and looked **once**. +At five seconds the dialog was there, the buttons were there, and the editor +was not — and the code's single failure message covered all three cases with +the sentence "the dialog did not open". + +Nothing was broken. The script was asking a question at the wrong time and +reporting the answer as a fact about LinkedIn. + +## The fix: poll for the node you need, not for the clock + +- **Poll for the specific node**, with a 40-second deadline, instead of + sleeping a fixed amount and checking once. +- **Name the missing precondition.** A timeout now says whether the dialog, + the buttons or the editor failed to appear. That single change would have + saved two sessions. +- **Retry the click once if no dialog appears at all.** A swallowed click is + a real second failure mode, and it deserves a different response than a + slow mount. +- **Leave a screenshot on failure.** The next person debugging this gets + pixels, not a sentence. +- **Remove the same single-shot look from the edit-and-verify modal**, which + had the identical shape on a different screen — the same bug in a second + place is not a coincidence, it is a pattern in how the code waits. +- **Tests:** 13 new browser-free tests pin the staged mount and the + swallowed click, so a future refactor that reintroduces `sleep(5)` fails + in CI instead of on my phone. + +## The second defect, which only publishing could reveal + +With the race fixed, the post went out — but not on the first attempt, and +the second failure is the more interesting one. The first click on "Post" +was **ignored for 75 seconds**. Then an identical click — same coordinates, +same DOM, and I verified the pointer sequence did reach the button's span — +was accepted in **2 seconds**. + +A click that is delivered and does nothing looks exactly like a click that +was never delivered. The only way to tell them apart is to stop treating the +click as the proof and make the *result* the proof: + +- the composer closing is now the only accepted confirmation of submission; +- a bounded three-attempt re-click, with a refusal to re-click a disabled + button; +- and no tearing the tab down mid-submission, which is what "cleaning up" + did while the request was still in flight. + +## The queue problem hiding behind the bug + +The bug was loud. The next failure would have been silent. + +My drafts are approved in batches with a 72-hour time-to-live, and the queue +drains at one post per day. A batch that was approved for a 12-day runway +therefore expired — all seven remaining rows — at 10:36 that same morning. +The next morning's tick would have found nothing usable and simply **not +posted**. Nobody gets an error message for work that no longer exists; a +scheduler that runs out of work goes quiet, and quiet looks like "nothing to +say today". + +I fixed only the expiration on the seven unused, hash-matching rows, leaving +status, hash and payload untouched, because whether a draft gets re-approved +is a human decision and the automation should not manufacture consent. The +durable fix — sizing the approval window to the queue's drain rate instead +of the batch's import date — is a decision I deliberately left open. + +## What I would do differently + +1. **Never `sleep(n)` and look once at anything that mounts + asynchronously.** Poll for the artifact you need, with a deadline and a + named failure. +2. **Treat a repeated false failure as evidence the first diagnosis was + wrong.** "The session may be dead" was a story that fit the symptom and + was never tested. +3. **Make the error message name the missing precondition.** Vague messages + do not just fail to help — they actively misdirect, and this one sent me + looking at sessions and markup for two days. +4. **Prove submission by the result, not by the click.** An input event is + not a side effect; the disappearance of the composer is. +5. **Monitor the queue separately from the job.** A job that runs perfectly + and posts nothing is indistinguishable from a working one unless you + count what came out. + +## Verification + +| Check | Result | +|---|---| +| Post live | published, with the permalink and activity URN read back from the page | +| Content | authenticated read-back confirms the body text and the bold phrase rendered correctly | +| Cron path | re-ran the job: `ok`, `failure_streak` back to 0 | +| Double-post guard | correctly refused a second post — "post cap reached: 1 done today" | +| Ledger | approval consumed and a `POST_PUBLISHED` row written with the permalink | +| Tests | 404 pass, including the 13 new ones for the staged mount and the swallowed click | +| Cleanup | the 11 tabs my probes opened were closed; the browser's original 4 were left as found | + +The post that went out, by the way, was the one about running speech-to-text +on my own hardware — +[the two-of-everything setup I described here](/posts/self-hosted-speech-to-text-api/). +It has nothing to do with this bug. That is rather the point: the job's +reporting was wrong about what had happened, and wrong reporting is worse +than no reporting. + +## Want this for your business? + +If you have a repetitive publishing, reporting or data-entry job that a +person is doing by hand — or an automation that "sometimes fails" and nobody +knows why — I build and debug browser and API automations on infrastructure +I run myself. Most of the work in a post like this is not the happy path; it +is making the failure honest enough to fix. + +**WhatsApp: [+60 12-797 2969](https://wa.me/60127972969)** · **Email: [me@hoelee.com](mailto:me@hoelee.com?subject=Automation%20that%20tells%20the%20truth)** · **[hoelee.com](https://hoelee.com)** + +I run Hoelee Enterprise, a website design and development business for +Malaysian SMEs — and the automation work above runs on the same +infrastructure I build for clients. If you are hiring for a platform, DevOps +or AI-infrastructure role, the same channels reach me. diff --git a/src/content/posts/zh/the-dialog-opened-in-7-seconds-my-script-waited-5.md b/src/content/posts/zh/the-dialog-opened-in-7-seconds-my-script-waited-5.md new file mode 100644 index 0000000..b70ac9d --- /dev/null +++ b/src/content/posts/zh/the-dialog-opened-in-7-seconds-my-script-waited-5.md @@ -0,0 +1,101 @@ +--- +title: "对话框 7.2 秒就打开了,而我的脚本只等了 5 秒" +description: "定时任务一直报「composer 对话框没有打开」。其实三个阶段分别挂在 1.8 秒、2.8 秒和 7.2 秒——而脚本只在第 5 秒看了一次。" +pubDate: 2026-10-15 +category: ai +tags: ["automation", "linkedin", "cron", "browser", "agents"] +ogImage: /og/the-dialog-opened-in-7-seconds-my-script-waited-5.png +banner: /banners/the-dialog-opened-in-7-seconds-my-script-waited-5.png +draft: true +--- + +## 一条技术上属实、方向上全错的失败消息 + +我那个定时发帖任务干不成活的时候会往我手机上发消息,那天早上它说: + +```text +⚠️ Cron 'linkedin-daily-post' failed: Script exited with code 1 + LinkedIn daily post — FAILED + FAILED: the composer dialog did not open +``` + +语气笃定、指认具体、毫无用处。没有任何内容被发出去,站点是好的,浏览器配置也还登着,而这条消息告诉我该怪谁:一个根本没出现过的对话框。 + +对话框出现了。它在我脚本判定它没出现之前 3.2 秒就已经出现了。 + +## 我先看的地方,以及为什么那是错的地方 + +一个对着网站上跑的自动化失败,通常有三个嫌疑对象:会话过期、网站改了结构、账号被限流。我先查了会话——健康。而在我自己的项目笔记里,四五天前的两次失败带着**一模一样**的消息,当时写下的工作假设是「会话可能已经死了」。 + +那句话就是线索。两次完全相同的失败、一个假设,而那个假设从未被验证过。一个反复出现、原因却没被确认的失败,是**量测问题**,不是诊断问题。 + +## 去量挂载时间,别去猜消息 + +于是我把浏览器保持在脚本留下的状态里打开 composer,给每一块 DOM 到达的时间记账: + +```text ++1.8 秒 composer 对话框元素存在 ++2.8 秒 Post / Proofread 按钮存在 ++7.2 秒 文本编辑器存在 <- 脚本真正要输入的那个节点 +``` + +脚本点了「开始发帖」,睡了五秒,然后看了**一次**。在第 5 秒,对话框在、按钮在、编辑器不在——而代码只有一条失败消息,用「对话框没有打开」这一句话覆盖了全部三种情况。 + +没有任何东西坏掉。是脚本在错误的时刻问了一个问题,然后把答案当成了一桩关于 LinkedIn 的事实。 + +## 修法:为你要的节点轮询,而不是为时钟 + +- **为具体节点轮询**,给 40 秒的期限,而不是睡一个固定时长再看一次。 +- **把缺失的前置条件说出来。** 现在超时会说明是对话框、按钮还是编辑器没出现。仅这一处改动就能省掉两个会话。 +- **如果对话框根本没出现,把点击重试一次。** 被吞掉的点击是另一个真实的失败模式,它值得一个与「挂载慢」不同的应对。 +- **失败时留下截图。** 下一个查这个 bug 的人应该拿到像素,而不是一句话。 +- **把编辑与校验弹窗里同一种「看一次」的写法也删掉**——那是另一个屏幕上完全相同的形状。同一个 bug 出现在第二处不是巧合,而是这段代码等待方式的模式。 +- **测试:** 新增 13 条不依赖浏览器的测试,钉住分阶段挂载与被吞掉的点击,于是将来任何重新引入 `sleep(5)` 的重构,会在 CI 里失败,而不是在我手机上失败。 + +## 第二个缺陷,只有真的发出去才看得见 + +竞态修好之后,帖子发出去了——但不是一次成功,而第二次失败更值得写。第一次点「Post」被**无视了整整 75 秒**。然后一次完全相同的点击(同样的坐标、同样的 DOM,我核对过指针事件序列确实到达了按钮的 span)在 **2 秒**内被接受。 + +一次「被送达但毫无作用」的点击,和一次「从未被送达」的点击,看起来完全一样。唯一能分辨两者的办法,是别再拿点击当证据,改拿**结果**当证据: + +- 现在只有 composer 自己关闭,才算提交被确认; +- 有上限的三次重试点击,并且拒绝对一个 disabled 按钮再点; +- 提交在途时不再把标签页拆掉——所谓「清理」在请求还在飞的时候就干过这件事。 + +## bug 背后藏着的是队列问题 + +这个 bug 很吵。而下一个失败会是**无声**的。 + +我的草稿是分批审批的,每批带 72 小时有效期,而队列每天只消耗一篇。于是一批按「12 天余量」审批的草稿,在当天上午 10:36 全部过期——剩下七行全部作废。第二天早上的定时任务会发现没有可用内容,然后**什么都不发**。「再也没有工作可做」不会产生任何错误消息;一个跑空了活的调度器只会安静下来,而安静看起来就像「今天没什么好发的」。 + +我只修了那七行未使用、哈希匹配记录的过期时间,状态、哈希、载荷一个字没动——因为草稿要不要重新批准是人的决定,自动化不该自己制造同意。至于那个更彻底的修法(让审批窗口按队列的消耗速率来定,而不是按批次导入的日期),我故意留着没做。 + +## 如果重来一次 + +1. **对任何异步挂载的东西,永远不要 `sleep(n)` 之后只看一次。** 为你要的产物轮询,带期限、带具名失败。 +2. **把一个反复出现的假失败,当成「第一次诊断错了」的证据。** 「会话可能已经死了」是一个贴合症状、却从未被检验的故事。 +3. **让报错消息说出缺失的前置条件。** 含糊的消息不只是没帮助——它主动把你带偏,这一条让我去查了两天的会话和页面结构。 +4. **用结果证明提交,而不是用点击。** 输入事件不是副作用;composer 消失才是。 +5. **把队列的健康和任务的健康分开监控。** 一个运行完美却没发出任何东西的任务,跟一个正常工作的任务长得一模一样——除非你去数它产出了什么。 + +## 验证 + +| 检查项 | 结果 | +|---|---| +| 帖子已上线 | 已发布,从页面回读了永久链接与 activity URN | +| 内容 | 认证态回读确认正文文字与加粗短语渲染正确 | +| 定时任务路径 | 重跑该任务:`ok`,`failure_streak` 回到 0 | +| 重复发布防护 | 正确拒绝第二次发布——「post cap reached: 1 done today」 | +| 台账 | 审批被消耗,并写入一条带永久链接的 `POST_PUBLISHED` 记录 | +| 测试 | 404 条通过,包含新增的 13 条(分阶段挂载、被吞掉的点击) | +| 收尾 | 我的探针打开过的 11 个标签页已关闭;浏览器原有的 4 个保持原样 | + +顺便说一句,那天真正发出去的那篇,是讲我把语音转文字跑在自己硬件上的那篇——[就是这里描述的那套「什么都是两份」的配置](/posts/self-hosted-speech-to-text-api/)。它跟这个 bug 毫无关系。而这恰恰是重点:这个任务的汇报对「发生了什么」说错了话,而错误的汇报比没有汇报更糟。 + +## 想要给你的业务做一套? + +如果你手上有一件重复性的发布、报表或数据录入工作正由人手工在做——或者有一个「偶尔会失败」但没人知道为什么的自动化——我在自己运维的基础设施上做浏览器与 API 自动化。像这样一篇文里的工作量,大部分不在顺风路径上,而在于**把失败弄得足够诚实,以至于能被修**。 + +**WhatsApp:[+60 12-797 2969](https://wa.me/60127972969)** · **邮箱:[me@hoelee.com](mailto:me@hoelee.com?subject=Automation%20that%20tells%20the%20truth)** · **[hoelee.com](https://hoelee.com)** + +我经营 Hoelee Enterprise,一家面向马来西亚中小企业的网站设计与开发公司——上面这些自动化工作,跑的就是我给客户搭建的那套基础设施。如果你正在招聘平台、DevOps 或 AI 基础设施方向的岗位,同样的渠道也能找到我。