feat(web): port PoW challenge page from vanilla JS to Preact#1522
feat(web): port PoW challenge page from vanilla JS to Preact#1522
Conversation
There was a problem hiding this comment.
Pull request overview
Ports the Proof-of-Work (PoW) challenge page frontend from imperative DOM manipulation to a declarative Preact implementation, aligning it with the project’s existing Preact usage for other challenge UIs.
Changes:
- Replaced
web/js/main.tswith a Preact-basedweb/js/main.tsxstate-machine UI mounted on a new#approot. - Updated the web asset build pipeline to bundle
.tsxvia esbuild JSX automatic runtime, and added an IDE-onlyweb/tsconfig.json. - Updated the PoW templ markup to provide a Preact mount point + fallback content; added challenge method to the “new challenge issued” log line; documented the change in the changelog.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| web/tsconfig.json | Adds an editor/typechecking config for TS/TSX with Preact JSX types. |
| web/js/main.tsx | New Preact implementation of the PoW UI and worker orchestration glue. |
| web/js/main.ts | Removes the prior vanilla/imperative implementation. |
| web/build.sh | Bundles .tsx inputs and passes JSX flags to esbuild for TSX builds. |
| lib/challenge/proofofwork/proofofwork.templ | Introduces #app mount point with server-rendered fallback and removes standalone progress DOM. |
| lib/challenge/proofofwork/proofofwork_templ.go | Regenerated templ output reflecting the updated .templ markup. |
| lib/anubis.go | Adds the challenge method to the “new challenge issued” structured log event. |
| docs/docs/CHANGELOG.md | Notes the PoW Preact rewrite in the Unreleased section. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (phase === "error") { | ||
| return ( | ||
| <> | ||
| <img style="width:100%;max-width:256px;" src={errorImage} /> | ||
| <p id="status">{errorMessage}</p> | ||
| </> | ||
| ); | ||
| } | ||
|
|
||
| if (phase === "loading") { | ||
| return ( | ||
| <> | ||
| <img style="width:100%;max-width:256px;" src={pensiveURL} /> | ||
| <p id="status">{t("calculating")}</p> | ||
| </> | ||
| ); | ||
| } |
There was a problem hiding this comment.
This seems kind of redundant. Probably could use the same <img> and <p id=status> tags that are inside the part // computing or reading below.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 33 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "missing_required_forwarded_headers": "Trūksta privalomų X-Forwarded-* antraščių" | ||
| } | ||
| "js_challenge_data_missing": "Trūksta iššūkio duomenų. Prašome perkrauti puslapį." | ||
| } No newline at end of file |
| data: string; | ||
| difficulty: number; | ||
| nonce: number; | ||
| } No newline at end of file |
There was a problem hiding this comment.
return the carriage or git gets mad
| <script async type="module" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version }></script> | ||
| <div id="progress" role="progressbar" aria-labelledby="status"> | ||
| <div class="bar-inner"></div> | ||
| <img style="display:none;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version }/> |
There was a problem hiding this comment.
Claude thinks happy.webp is vestigial from #745 and loaded without use. Same in the other templates.
But it's not part of this PR.
| "jsxImportSource": "preact", | ||
| "noEmit": true, | ||
| "skipLibCheck": true, | ||
| "strict": false |
There was a problem hiding this comment.
Maybe strict mode could find some issues with null checks and so on?
SlyEcho
left a comment
There was a problem hiding this comment.
I think it's fine. Some minor nits
web/js/main.tsx
Outdated
| >("loading"); | ||
|
|
||
| // Error info | ||
| const [errorTitle, setErrorTitle] = useState(""); |
There was a problem hiding this comment.
This is not used any longer.
web/js/main.tsx
Outdated
| } | ||
|
|
||
| const showError = (title: string, message: string, imageSrc: string) => { | ||
| setErrorTitle(title); |
SlyEcho
left a comment
There was a problem hiding this comment.
I'd remove the errorTitle state.
| // the probability of still being on the page is (1 - likelihood) ^ iters. | ||
| // by definition, half of the time the progress bar only gets to half, so | ||
| // apply a polynomial ease-out function to move faster in the beginning | ||
| // and then slow down as things get increasingly unlikely. quadratic felt | ||
| // the best in testing, but this may need adjustment in the future. | ||
|
|
||
| const probability = Math.pow(1 - likelihood, iters); | ||
| const distance = (1 - Math.pow(probability, 2)) * 100; | ||
| setProgress(distance); | ||
|
|
||
| if (probability < 0.1 && !apologyShown) { | ||
| apologyShown = true; | ||
| setShowApology(true); | ||
| } |
There was a problem hiding this comment.
Maybe this could be moved inside the if above, so the progress and apology also updates only once-per-second?
Replace the imperative DOM manipulation in web/js/main.ts with a
declarative Preact component (web/js/main.tsx). The project already
uses Preact for the timed-delay challenge (lib/challenge/preact/),
so this aligns the PoW challenge with the existing codebase direction.
Convert web/js/main.ts to a Preact TSX component. The worker
orchestration layer (web/js/algorithms/fast.ts) stays untouched --
it is already cleanly separated and works via a Promise API.
web/js/main.ts -> web/js/main.tsx:
- Phase-based state machine (loading -> computing -> reading/error)
replaces scattered imperative DOM updates.
- Worker lifecycle managed in useEffect; progress callback drives
state setters for speed and progress percentage.
- Speed updates remain throttled to 1 second intervals.
- i18n functions (initTranslations, t(), loadTranslations) kept as
module-level state -- no need for React context in a single-
component app.
- The <details> section stays in the templ file as server-rendered
HTML; the Preact component tracks its toggle state via useRef.
- Uses esbuild automatic JSX transform (--jsx=automatic
--jsx-import-source=preact) instead of classic pragmas.
web/build.sh:
- Add js/**/*.tsx to the glob so esbuild bundles TSX files.
- Pass --jsx=automatic --jsx-import-source=preact for .tsx files.
web/tsconfig.json (new):
- IDE-only config (noEmit) so TypeScript understands Preact JSX
types for editor diagnostics and autocompletion.
lib/challenge/proofofwork/proofofwork.templ:
- Replace individual DOM elements (img#image, p#status,
div#progress) with a <div id="app"> Preact mount point
containing server-rendered fallback (pensive image + loading
text).
- Keep <details>, <noscript>, and <div id="testarea"> outside the
Preact tree as server-rendered content.
lib/anubis.go:
- Add challenge method to the "new challenge issued" log line.
docs/docs/CHANGELOG.md:
- Add entry for the Preact rewrite.
- web/js/algorithms/fast.ts -- untouched
- web/js/algorithms/index.ts -- untouched
- web/js/worker/sha256-*.ts -- untouched
- Server-side Go code (proofofwork.go) -- untouched
- JSON script data embedding -- untouched
- Redirect URL construction -- same logic, same parameters
- Progress bar CSS in web/index.templ -- untouched
Signed-off-by: Xe Iaso <me@xeiaso.net>
Assisted-by: Claude Opus 4.6 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
Translate the new challenge data missing error message across all 25 supported locales. Assisted-by: Claude Opus 4.6 via Claude Code Signed-off-by: Xe Iaso <me@xeiaso.net>
- Remove duplicate style attribute on happy image preload in templ - Remove unused getAvailableLanguages function from main.tsx - Add type annotation to currentLang variable - Add null check for missing challenge data with localized error Assisted-by: Claude Opus 4.6 via Claude Code Signed-off-by: Xe Iaso <me@xeiaso.net>
- Guard j() helper against null/empty textContent before JSON.parse - Use <button> instead of clickable <div> for "finished reading" control to support keyboard navigation and screen readers - Make currentLang a local const inside initTranslations since it is not read elsewhere Assisted-by: Claude Opus 4.6 via Claude Code Signed-off-by: Xe Iaso <me@xeiaso.net>
Signed-off-by: Xe Iaso <me@xeiaso.net>
… function Signed-off-by: Xe Iaso <me@xeiaso.net>
Signed-off-by: Xe Iaso <me@xeiaso.net>
Replace <br/> tags with em dashes and strip all other HTML tags (keeping their text content) across all 25 locale files. Assisted-by: claude-sonnet-4-6 via Claude Code Signed-off-by: Xe Iaso <me@xeiaso.net>
Use literal spaced em dash ( — ) consistent with other locale files instead of unspaced \u2014 escape sequence. Assisted-by: claude-sonnet-4-6 via Claude Code Signed-off-by: Xe Iaso <me@xeiaso.net>
The process() function in fast.ts declared Promise<string> but actually
resolved with the full worker message object { hash, data, difficulty,
nonce }. main.tsx papered over this with `any`. Introduce a shared
ChallengeResult type so the contract between workers, algorithms, and
the main component is enforced by the type checker.
Assisted-by: Claude Opus 4.6 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
The useEffect registered a toggle listener on the <details> element but never removed it, which could leak handlers on remounts or in tests. Extract the handler to a named function and return a cleanup that calls removeEventListener. Assisted-by: Claude Opus 4.6 via Claude Code Signed-off-by: Xe Iaso <me@xeiaso.net>
Signed-off-by: Xe Iaso <me@xeiaso.net>
a8f9efc to
cada118
Compare
Closes: #1149
This is an experimental use of Claude Opus 4.6 via Claude Code to do a fairly nontrivial transformation in the Anubis project. Fine-combed review is welcome and encouraged. The rest of this PR body is generated by Claude and contains the plan that it generated when doing the big rewrite.
Civil comments in the thread are welcome. Uncivil comments will be deleted. Please take your debates about the tools being used to another forum. This use of Claude Opus is clearly disclosed in both the
Assisted-byfooter and by me stating that I used Claude Opus to do this work while recovering from major surgery.I am not paying Anthropic for this use of Claude Opus. They gave me free access with their open source program. I was surprised that I got in it, but it's been a neat tool to use regardless.
Replace the imperative DOM manipulation in web/js/main.ts with a declarative Preact component (web/js/main.tsx). The project already uses Preact for the timed-delay challenge (lib/challenge/preact/), so this aligns the PoW challenge with the existing codebase direction.
Approach
Convert web/js/main.ts to a Preact TSX component. The worker orchestration layer (web/js/algorithms/fast.ts) stays untouched -- it is already cleanly separated and works via a Promise API.
What changed
web/js/main.ts -> web/js/main.tsx:
<details>section stays in the templ file as server-rendered HTML; the Preact component tracks its toggle state via useRef.web/build.sh:
web/tsconfig.json (new):
lib/challenge/proofofwork/proofofwork.templ:
<details>,<noscript>, and<div id="testarea">outside the Preact tree as server-rendered content.lib/anubis.go:
docs/docs/CHANGELOG.md:
What stayed the same
Assisted-by: Claude Opus 4.6 via Claude Code
Checklist:
[Unreleased]section of docs/docs/CHANGELOG.mdnpm run test:integration(unsupported on Windows, please use WSL)