Skip to content

v3.10.0 — recipe append blocks, /api/status sidecar health - #53

Merged
syswave-dev merged 13 commits into
mainfrom
feat/append-and-status
Aug 27, 2026
Merged

syswave-dev merged 13 commits into
mainfrom
feat/append-and-status

Conversation

@syswave-dev

Copy link
Copy Markdown
Collaborator

v3.10.0 — recipe append blocks, sidecar health endpoint

Two independent features that accumulated on top of 3.9.0.

append: pull numeric data out of SSR state blobs

Single-page frameworks serialize their initial state into a <script> block (ng-state, __NEXT_DATA__, …) and then render it as a chart. The numbers are in the HTML, but no CSS selector reaches them, so extraction returns prose and drops the data the page is actually about.

The new optional recipe field append names a script selector and a path into the JSON, optionally projects a subset of fields and caps the row count, and appends the result as a fenced JSON code block at the end of the document. Output size is capped, and the block is attached on every extraction path — but only after the render decision, so it cannot influence which extractor wins.

Deliberately not documented in SITE-RECIPES.md: this is an internal escape hatch for instance-local recipes, not a contributor-facing feature.

GET /api/status: make a dead sidecar visible

The Playwright sidecar on the production instance crash-looped for eight weeks without anyone noticing. Nothing looked broken from outside — the service kept answering 200, extraction just silently fell back to near-empty results for every page that needs a browser render.

{"ok":false,"version":"3.10.0","services":{
  "trafilatura":{"status":"ok","latencyMs":3},
  "playwright":{"status":"down","error":"unreachable"},
  "markitdown":{"status":"not-configured"}}}
  • Probes each configured sidecar's /health in parallel; 200 when they all answer, 503 as soon as one is down, so a plain HTTP monitor catches it without keyword matching.
  • A sidecar that was never configured is not-configured, not a failure — running without Playwright or markitdown stays a supported setup.
  • Error reasons are a closed vocabulary (unreachable, timeout, unhealthy, http <code>, misconfigured). The endpoint is public and a raw error message would carry the internal sidecar hostname; a test asserts that.
  • Ungated like the other aggregate endpoints, because a monitor polls it without credentials. Results are cached 5 s and concurrent callers share one round of probes.

Also pins playwright-stealth, the one unpinned sidecar dependency, to the version already running in production.

Verification

  • node --test: 1251 pass, 0 fail. markitdown-sidecar/test_limits.py + test_youtube.py: 13 pass.
  • 22 new tests across test/status.test.js and test/status-endpoint.test.js.
  • Live-verified on the production instance: /api/status returns 200 with all three sidecars healthy; the 503 path verified against a real server instance with unreachable sidecar URLs.

🤖 Generated with Claude Code

https://claude.ai/code/session_018W84fQHmxwu4S519Zd4Vde

syswave-dev and others added 13 commits August 9, 2026 11:45
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e cap

Heading, fence markers and the truncation note were assembled after the
byte budget was spent on the row body alone, so a long title could push
the emitted block (and the document) past BLOCK_BYTE_CAP/DOC_BYTE_CAP.
Reserve the worst-case chrome size before serializing rows and hand only
the remainder to serializeRows; skip the block if that leaves less than
MIN_USEFUL_BUDGET for the body.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The old finish() wrote the append block into result.markdown before
extractWeb() ran renderDecision(), which measures result.markdown
length. A large enough append block could push a thin static
extraction past the 500-char floor and silently skip the Playwright
re-render on exactly the SPA pages this feature targets.

finish() now stashes { markdown, notes } on result.append instead of
attaching. A new attachAppendBlock() helper does the attaching
(markdown, contentLength, extractorReason - same as before) and is
called only once the render decision is settled: on the no-render
path, on the rendered path (after the existing extractorReason
assignment, so the render note survives instead of being overwritten),
on the sidecar-failure path, and in extractHtml() (which never goes
through renderDecision at all).
Fixes seven findings from the whole-branch review of the recipe `append`
field (structured-data extraction from embedded JSON state blobs into a
fenced JSON block at the end of the document):

1. lib/web.js: rewrote the stale comment above the append-block render
   call, which claimed finish() attaches the block on every return path.
   It only stashes it on result.append; the callers attach it via
   attachAppendBlock() once the render decision is settled.
2. lib/recipes.js: append-block `title` now rejects line breaks via a
   regex, so a newline in the title can no longer open the code fence
   early and leave the JSON body unfenced.
3. lib/append-data.js: resolveSegments() now guards the string-segment
   branch with Object.prototype.hasOwnProperty and rejects string
   segments against arrays, so inherited properties (constructor,
   __proto__) and exotic array properties (length) no longer resolve.
4. lib/append-data.js: the kept === 0 skip note (a single row exceeds
   the block's byte cap) now says so explicitly, instead of reusing the
   "Budget erschöpft" wording that belongs to the genuine document-
   budget-exhaustion case a few lines above.
5. lib/append-data.js: the row-count note and truncation text now use
   the German singular "Zeile" when exactly one row is kept, via a small
   rowNoun() helper, instead of always saying "1 Zeilen".
6. test/recipes-append.test.js: added a test for the comments=true
   return path (extractWeb + injected fetch + render: 'skip'), which
   extractHtml can never reach since it hardcodes comments = false.
7. CHANGELOG.md: extended the HTML comment under [Unreleased] to note
   that the reference-link list at the end of the file also needs a
   matching entry when the release is cut.

Test growth: 1223 -> 1229 passing (+6), 288 -> 289 suites (+1). All
green, no other output.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Playwright sidecar on the production instance crash-looped for eight
weeks without anyone noticing. Nothing was broken from the outside: the
service kept answering 200, extraction just silently fell back to empty
results for every page that needs a browser render.

Add an endpoint that makes that state observable. It probes the /health
route of each configured sidecar in parallel, answers 200 when they all
respond and 503 as soon as one is down, so a plain HTTP monitor catches
it without keyword matching. An unconfigured sidecar is not a failure -
running without Playwright or markitdown is a supported setup.

Error reasons stay a closed vocabulary (unreachable / timeout /
unhealthy / http <code> / misconfigured) because the endpoint is public
and a raw error message would carry the internal sidecar hostname.
Results are cached for 5s and concurrent callers share one round of
probes, so polling cannot amplify into internal traffic.

Also pin playwright-stealth, the one unpinned sidecar dependency: a
rebuild could otherwise have pulled an incompatible release unnoticed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018W84fQHmxwu4S519Zd4Vde
Two features in this batch: recipe append blocks, which lift numeric
data out of SSR state blobs where no CSS selector can reach it, and
GET /api/status, which makes a dead extraction sidecar visible instead
of letting it degrade the output in silence.

The 3.x list in README and help page names only the status endpoint.
The append field stays deliberately undocumented outside the changelog:
it is an internal recipe escape hatch, not a contributor-facing feature,
so SITE-RECIPES.md does not mention it either.

node --test: 1251 pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018W84fQHmxwu4S519Zd4Vde
@syswave-dev
syswave-dev merged commit 80844f2 into main Aug 27, 2026
7 of 8 checks passed
@syswave-dev
syswave-dev deleted the feat/append-and-status branch August 27, 2026 07:26
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.

1 participant