feat(app): the app scans on its own, WebView included (phase 16) - #20
Merged
Conversation
A half phase, numbered like one. It adds no capability — it decides what the app looks like once there is something real to look at. Slotting it between 16 and 17 rather than appending it leaves every number below untouched, which matters because phase numbers are cited from commits, prompt files and phases.json. It comes after 16 rather than before, deliberately. Designing against a fixture is designing against invented content; once the app scans for real there are real screens — a long scan reporting progress, eighty products, an empty result, an error — and those are what a design has to hold. Records what a redesign may not drop, because both are §18 requirements rather than taste: the currency confirmation keeps its own visual weight and never becomes a skippable checkbox, and directional rules stay logical properties or the Persian layout silently stops being right. The Android design pass is noted as a separate half phase after 18 and deliberately left unspecified — the phone is a different form factor and should get its own decisions rather than a shrunk desktop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 16, most of it. The app fetches through Rust, runs `core`'s pipeline, and writes a file — no browser involved at any point. Verified against a real Persian WooCommerce shop: 43 rows, three variable parents and their variations, exported and checked against §7. BOM present, parent price cells empty, no orphan variations, every SKU unique, prices ASCII, and 75 variation attribute values every one of which appears verbatim in its parent's option list (§7.6). **`runScan` moved from the extension into `core`.** §2 calls a feature that lands on one surface a design failure, but what made this non-negotiable rather than merely tidy is `countCurrencyUnits`: it is what lets a surface ask §7.8's toman/rial question before writing a file, and a second copy of that rule is exactly how this project produces a silent ten-times price error. The extension now imports it and behaves identically. **The pacing split is the subtle part.** `crawlCategory` wraps whatever client it is handed in a polite client of its own, so it must receive the raw one — passing an already-paced client would wrap it twice, pacing the whole crawl at double the configured delay and double-counting its requests. The app's own first fetch, which happens before `runScan` exists to do it, gets its own paced wrapper so the very first request a shop ever sees is already subject to §10. Rust stays a transport. It moves bytes and returns status and headers; it does not decide how fast to move them or what a 403 means. Pacing is `core`'s, block detection is `core`'s, and there are no retries, because a retry that pushes past a block is what §2 forbids. Also fixes two real bugs found by running it: **HTML entities were reaching the CSV undecoded.** An SEO plugin writes the product name into JSON-LD with HTML escapes intact and `JSON.parse` has no reason to undo them, so a real product exported as `… تک سیم کارت فیزیکی – Not Active`. `cleanText` now decodes, which is the right place because parent options and variation values both pass through it — decoding only one of the two paths would have been worse than decoding neither. **Scan did nothing.** The button's disabled state was set when it was built, while the field was empty, and typing never told it otherwise. It is updated from the input handler now rather than by re-rendering, which would rebuild the field and take the caret with it. Settings gains the real `Proc123Config` — exporter, content mode, page limit, politeness, default unit — with the two warnings that matter said out loud: shortening the delay puts more load on somebody else's server, and copied descriptions are the shop's content. The default unit is explicitly not an answer to §7.8; every export still asks. About states §15's no-account promise and §2's hard constraint where a user can read them rather than only in a repository. 835 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 16's last piece, and the failure the CLI has always had: a shop that builds its grid in JavaScript ships an empty shell to anything that only reads HTML, and reads as zero products. The app now renders the page in a real WebView and reads it back. Two capabilities, not one. `rendered_html` is what this phase needs; `evaluate` is what phase 27 needs to read Core Web Vitals out of the Performance API, and it is written now because adding it later means reopening the window plumbing rather than writing one more command. The render windows get none of Tauri's IPC. They load somebody else's page, and a scanned shop that could reach `invoke` would have the filesystem and HTTP client of this process. Only http(s) URLs are rendered — a `file:` URL arriving from a scanned page's own markup would point the renderer at the local disk. **Five bugs, every one of them ours rather than the platform's**, and each presented as "this site does not render": - wry queues a script while the page is loading and drops the callback without calling it, so an unanswered `eval` means *not loaded yet* and never *broken*. Treating it as failure returned the pre-JS shell every single time, which is exactly what the renderer exists to avoid. - A window that is never mapped never starts its webview, so `visible(false)` silently prevented the page from loading at all. - A time budget has to measure time. Adding a poll interval per iteration counts turns, and an iteration waiting on an unanswered eval takes far longer — a "15 second" ceiling kept a window on screen for minutes. - A cheap probe and a megabyte payload cannot share a timeout. Two seconds is right for `outerHTML.length` and made the real read come back empty, which reads exactly like a page that rendered nothing. - Loading and settling need separate budgets. Sharing one meant a slow first load spent the whole allowance before a single useful measurement: the same page returned 415KB when it loaded fast and nothing when it did not. The fallback is gated on *zero* products rather than "fewer than expected". Rendering costs a second page load plus every subresource, which is real load on somebody's server (§10), and a partial result is a real result — an empty one is the only case with nothing to lose. Verified on live Persian shops: kgkala 43 rows via Layer A, digikala rendered 415KB and 379 products, technolife rendered 1.25MB, zitu 414KB. **Rendering is not extraction**, and that is the finding to carry forward. technolife renders its whole grid and yields nothing, because the rendered page carries ld+json=2 (Organization and Breadcrumb), itemtype=0 and schema.org/Product=0. Layer B is correct — there is no structured data there. Those shops need Layer C over the *rendered* DOM, which is phase 19 and which this phase makes possible without delivering. Also adds the logging that made any of this diagnosable. A Layer A failure used to look identical to a page with no markup; §11 says a failure should be able to explain itself. Phase 16 closes; 17 becomes next. phase-17.md written, and the five bugs recorded in packages/app/README.md so they are not rediscovered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
Phase 16 complete. The app fetches through Rust with no CORS, runs
core's pipeline, renders client-rendered shops in an embedded WebView, and writes a CSV straight to disk. No browser involved at any point.Earlier commits on this branch landed HTTP, the scan wiring and the save dialog, and those were verified against a real shop. This one adds the WebView — the failure the CLI has always had, where a JS-built shop reads as zero products.
Two capabilities, not one.
rendered_htmlis what this phase needs.evaluateis what phase 27 needs to read Core Web Vitals out of the Performance API, and it is written now because adding it later means reopening the window plumbing rather than writing one more command.The render windows get none of Tauri's IPC. They load somebody else's page, and a scanned shop that could reach
invokewould have the filesystem and HTTP client of this process. Onlyhttp(s)URLs are rendered — afile:URL arriving from a scanned page's own markup would point the renderer at the local disk.Five bugs, and every one of them was ours
Worth reading, because each presented identically as "this site does not render" and they are recorded in
packages/app/README.mdso the next session does not rediscover them:webkitgtk/mod.rspushes the script ontopending_scriptsand returnsOk(())without ever calling back. So an unansweredevalmeans not loaded yet, never broken — treating it as failure returned the pre-JS shell every single time.visible(false)is the obvious choice and silently prevents the page loading at all.outerHTML.lengthand made the real read come back empty — which reads exactly like a page that rendered nothing.The fallback is gated on zero products, not "fewer than expected". Rendering costs a second page load plus every subresource — real load on someone's server (§10) — and a partial result is a real result. An empty one is the only case with nothing to lose.
How it was verified
npm run checkpasses, 835 tests.cargo fmt,clippy -D warningsandcargo testclean.Against live Persian storefronts, not fixtures:
The finding that matters most
Rendering is not extraction. technolife renders its entire grid and yields nothing, because the rendered page carries
ld+json=2(Organization and Breadcrumb),itemtype=0andschema.org/Product=0. Layer B is behaving correctly — there is no structured data to read.So the WebView fixes "the HTML was empty". It does not fix "this shop publishes no structured data" — that is Layer C's job (phase 19), and the two are meant to compose: render first, extract from the rendered DOM.
phase-17.mdcarries that into the next session.This also adds the logging that made any of the above diagnosable. A failing Layer A used to look identical to a page with no markup; §11 says a failure should be able to explain itself, and it could not.
Checklist
npm run checkpasses (format, lint, typecheck, tests)🤖 Generated with Claude Code