Commit Graph

51 Commits

Author SHA1 Message Date
Kazuki Yamada e4a635c2f2 fix(website): Address PR review feedback on isBot pre-mint guard
Four items from gemini and claude reviews:

- botDetect.ts: Memoize isBot() result. navigator.userAgent is immutable
  for the page lifetime, so re-running the isbot regex on every Turnstile
  pre-mint debounce and post-submit re-mint check is wasted work. SSR
  fallback is intentionally not cached so a module instance reused across
  SSR/CSR still reaches the real UA check on first CSR call.
- usePackRequest.ts: Disambiguate the "submit-path NOT gated" comment —
  it was confusing because the new post-submit re-mint also lives inside
  submitRequest's finally. Reworded to "click-path acquireTurnstileToken"
  to make clear which call site is intentionally skipped.
- usePackRequest.ts: Update the userTouched comment to reflect autofill
  reality — modern Chromium/Firefox DO fire input events on autofill, so
  the rationale ("autofill doesn't trigger") was already stale. The new
  isBot() guard covers the gap for well-behaved crawler UAs.
- usePackRequest.ts: Add English glosses for the Japanese CF dashboard
  labels (提示チャレンジ / 未解決) so non-Japanese-reading maintainers can
  follow the comment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 20:07:08 +09:00
Kazuki Yamada d94475ef01 perf(website): Skip Turnstile pre-mint for bot-shaped user agents
The CF Turnstile dashboard shows ~17k unsolved challenges over the past
7 days vs ~10k solved — a 2:1 unsolved:solved ratio that's larger than
the post-submit auto re-mint can explain. Most of those are JS-executing
crawlers (Slackbot, Discord card validator, Twitter card validator, X,
Apple link preview, Googlebot, etc.) that render the page, somehow trigger
a DOM input event on the URL field (autofill / accessibility tools /
focus tricks), and pay a CF challenge they can't solve.

Add an `isBot()` short-circuit at the two pre-mint call sites in
`usePackRequest`:
  - the debounced `onTrigger` after `markUserTouched` flips
  - the post-submit auto re-mint in `submitRequest`'s finally block

The actual security gate is the server-side `siteverify` in
`turnstileMiddleware` — that stays the only authoritative check, so a
crawler that spoofs UA past `isBot()` still gets blocked there. The
submit-path `takeToken()` is intentionally NOT gated to avoid
false-positive lockouts of legitimate users with unusual UAs (e.g. older
clients, accessibility tools).

Net effect:
  - "提示チャレンジ" / "未解決" CF dashboard counters drop sharply
    (well-behaved crawlers stop minting unsolvable tokens)
  - `pack_completed` server logs unaffected (legit users don't change
    paths; bots couldn't reach `/api/pack` either way)
  - server-side spend on siteverify unchanged

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 19:56:19 +09:00
Kazuki Yamada 54c6a3d238 fix(website): Address claude third-pass review on siteverify metric
Six items from claude's incremental review (`12:48:43Z`):

- monitoring/dashboard.json: Group the outcomes widget by both
  `metric.label.outcome` and `metric.label.reason`. Previously all
  failures collapsed into a single `turnstile_failed` series, which
  contradicted the README claim that the `reason` label drives the
  breakdown.
- monitoring/metrics/*.yaml: Narrow the metric filter to
  `jsonPayload.event=("turnstile_siteverify" OR "pack_completed")`.
  Without this anchor, any future code path attaching
  `siteverifyDurationMs` to an unrelated log silently joins the
  distribution and creates new metric label values.
- usePackRequest.ts: Mirror `progressMessage.value = null` alongside
  the `progressStage.value = null` clear on token-acquisition aborted /
  error branches. Prevents a future edit setting a verifying message
  from leaking prior-run state.
- turnstile.test.ts: Add a focused `describe` block with five tests
  asserting `siteverifyDurationMs` is attached to every post-siteverify
  log (one success path + four reject branches). The metric YAML
  filters on field presence, so a refactor that drops the field on any
  branch would silently break the metric without other tests failing.
  Uses the existing `vi.spyOn(logger, ...)` pattern; no clock injection
  needed.
- monitoring/README.md: Note that the metric filter pins
  `service_name="repomix-server-us"`, so future regions (`-eu`,
  `-asia`) silently drop out until the filter is broadened or
  per-region counterparts applied.
- monitoring/README.md: Add a `gcloud logging metrics describe` snippet
  for verifying a YAML edit was actually applied (gcloud update is
  silent on no-op vs effective change).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 21:59:53 +09:00
Kazuki Yamada fa06e5059c fix(website): Address PR review feedback on siteverify metric
Six items from gemini, claude initial review, and claude follow-up:

- turnstile.ts: Update misleading comment that claimed the metric filters
  on `event=turnstile_siteverify` and `outcome=success`. The actual
  Cloud Monitoring metrics in `monitoring/metrics/` filter on
  `siteverifyDurationMs` field presence, which uniformly captures both
  the parallel success log (event=turnstile_siteverify) and the four
  rejectAndLog failure paths (event=pack_completed). The comment
  contradicted README and YAML and would mislead future readers.
- turnstile.ts: Wrap rejectAndLog in a local `rejectWithDuration` helper
  so every post-siteverify branch automatically carries
  `siteverifyDurationMs`. Prevents drift if a fifth reject reason gets
  added later.
- client.ts: Split the wire-protocol `PackProgressStage` (server-emitted
  SSE values) from the display-only `DisplayProgressStage` superset that
  adds `verifying`. Keeping the synthetic stage out of the wire type
  prevents silent divergence with the server's `PackProgressStage`.
- usePackRequest.ts, TryItLoading.vue, TryItResult.vue: Switch the
  display-side type to `DisplayProgressStage`. `onProgress` callbacks
  still take the wire `PackProgressStage`.
- usePackRequest.ts: Clear `progressStage` on token-acquisition failure
  branches (aborted / error). Functionally invisible since loading=false
  hides the loading UI, but prevents the next submit's verifying flash
  from briefly showing the previous run's stale state.
- monitoring/metrics/turnstile_siteverify_duration.yaml: Retune the
  exponential bucket layout for the 100ms-1s SLO band where decisions
  get made. Doubling buckets only placed ~3 boundaries between 100ms
  and 1s; growthFactor=1.5 with scale=10 places ~8 boundaries there.
  18 finite buckets cover 10ms to ~9.85s, comfortably above the 5s
  siteverify timeout so timeouts don't land in overflow.
- monitoring/README.md: Document that pre-network rejections
  (secret_missing, missing_token, token_too_long) intentionally don't
  carry siteverifyDurationMs, so they're excluded from both metrics
  but still appear in the existing pack_requests metric.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 21:42:21 +09:00
Kazuki Yamada 35c56abd02 perf(website): Show verifying step + emit siteverify duration metric
Two changes targeting the visible "..." gap between Pack click and the
first SSE progress event observed after PR #1544 landed:

- Client: add a synthetic `verifying` PackProgressStage so the loading
  UI displays "Verifying request..." while the server runs Turnstile
  siteverify (typically 100-1000ms before the first 'cache-check' SSE
  event arrives). The first onProgress callback from handlePackRequest
  overwrites it with the real server-reported stage.

- Server: time the siteverify round-trip in `turnstileMiddleware` and
  emit `siteverifyDurationMs` on every outcome (success / network
  failure / rejected / action mismatch / hostname mismatch). Success
  path adds a structured log with `event: turnstile_siteverify` so
  Cloud Monitoring can build a log-based distribution metric for
  p50/p95/p99 latency and alert on regressions during Cloudflare
  incidents.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 19:19:01 +09:00
Kazuki Yamada 6fe66f172a revert(website): Drop ?repo= -> userTouched flip to avoid amplification
A valid `?repo=` permalink was treated as an intent signal so the
visitor's click path used a pre-minted token. claude's follow-up review
flagged that this re-creates the dashboard counter inflation this PR is
meant to fix: any third-party page driving traffic to
`https://repomix.com/?repo=<owner/repo>` (Slack / Discord / Twitter card
validators that execute JS) would mint a token per visit, regardless of
whether the visitor ever submits.

Permalink visitors now pay the cold mint on their first click; the user's
first real form interaction (typing, mode click, option tweak, file
upload) is what gates the pre-mint.

Also document the idle-tab cliff in `expired-callback`: re-arming
pre-mint there would burn a challenge every 240s for the lifetime of
an open tab, which is worse than the cold-mint-on-return cost it would
save.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:37:59 +09:00
claude[bot] 23494e5c8d fix(website): Remove leftover useTurnstile.error references
Commit 2a0922d dropped the `error` ref declaration from `useTurnstile`
but left two references behind — `error.value = null` at the top of
`mintToken()` and `error` in the returned object. The first throws
ReferenceError at runtime; the second is a tsc error. No external
caller reads `useTurnstile().error` (TryIt.vue only consumes
`usePackRequest.error`), so the export was already vestigial.

Co-Authored-By: Kazuki Yamada <yamadashy@users.noreply.github.com>
2026-05-05 13:34:07 +00:00
Kazuki Yamada 2a0922d114 refactor(website): Address claude follow-up review on Turnstile pre-mint
- Drop the unused `error` ref from `useTurnstile`. The widget-level
  error-callback writes had no observer (only `usePackRequest.error`
  feeds the UI), so the export and writes were vestigial.
- Drop `getResponse` from `TurnstileGlobal`. Never called anywhere in
  the codebase; clearer to leave it off the typed surface.
- Don't `console.warn` on normal cancel/timeout flows in
  `acquireTurnstileToken`. Move the warn after the `signal.aborted`
  check so the dev console only logs genuine challenge / script-load
  failures.
- Hoist the consecutive `if (widgetId.value)` guards in `mintToken` by
  capturing the rendered widget id into a local const after the throw.
- Drop the redundant `userTouched.value` check in the post-pack
  pre-mint guard. `userTouched` is necessarily true at this point —
  it was a precondition for `isSubmitValid` being true when the submit
  started.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:28:33 +09:00
Kazuki Yamada 2ee93b3214 refactor(website): Address remaining PR feedback on Turnstile pre-mint
- Mark `userTouched=true` when arriving via a valid `?repo=` permalink so
  the visitor's click path uses a pre-minted token. Browser autofill and
  malformed `?repo=` values still don't burn a challenge.
- After a non-aborted submit completes, schedule a fresh `preMintToken()`
  in the finally block. Warms the cache for the typical "view result →
  tweak options → repack" flow and for `repackWithSelectedFiles`.
- Reduce the pre-mint debounce from 500ms to 300ms. Tightens the window
  where a paste-and-click cadence misses the cache.
- Split composables to fit the 250-line file-size guideline:
  * Extract token cache (cache state + single-flight mint + atomic
    one-shot consumption) into `useTurnstileTokenCache.ts`. Shrinks
    `useTurnstile.ts` from 358 → 241 lines and lets the widget file
    focus on render lifecycle / supersede logic.
  * Extract pre-mint debounce trigger into `usePreMintDebounce.ts`.
  * Extract Turnstile token acquisition + user-facing failure copy into
    `turnstileSubmit.ts`. Drops `usePackRequest.ts` from 345 → 331
    lines; `submitRequest` is a single cohesive request lifecycle that
    resists further splitting.
- Drop the unused `consumed` flag on `CachedToken` (claude review). The
  cache nulls the entry on consumption instead, which is what the
  takeToken atomic-claim loop already relies on.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:01:25 +09:00
Kazuki Yamada b9a9e719f8 fix(website): Address PR review feedback on Turnstile pre-mint
- Drop the unused `invalidateCache` export from useTurnstile. Both call
  paths (takeToken cache claim, expired-callback) already null
  cachedToken inline, so the helper had no callers.
- Update stale `turnstile.getToken()` references in usePackRequest and
  useTurnstileScript comments to match the renamed `takeToken()` /
  `preMintToken()` API.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:41:48 +09:00
Kazuki Yamada 01a1d237b9 fix(website): Address codex review on Turnstile pre-mint flow
- useTurnstile: Make takeToken() one-shot under concurrency. Two callers
  awaiting the same shared mintPromise both received the same token,
  which siteverify rejects as `timeout-or-duplicate`. Claim the cache
  atomically post-await and loop into a fresh mint if another caller won.
- usePackRequest: Drop pending pre-mint debounce timer at submitRequest
  start and on unmount, and skip scheduling while loading is true. Stops
  a debounce-firing-during-submit from minting an extra Turnstile
  challenge alongside the click path's mint.
- TryItPackOptions: Emit userInput from option handlers and wire to
  markUserTouched in TryIt. Without this, users hydrating via `?repo=`
  who only tweak format/include patterns/checkboxes never tripped the
  pre-mint gate, so their click path always cold-minted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:54:28 +09:00
Kazuki Yamada bef4c4a805 fix(website): Share mint promise between pre-mint and click paths
intent(fast-click-race): When the user clicked Pack within the 500 ms pre-mint debounce window, takeToken() cold-pathed into mintToken() *and* the debounce timer later fired preMintToken() which started a second mintToken(). The generation-counter supersede logic in mintToken() rejected the first call as "Superseded by new Turnstile request", so the user's own click surfaced as a verification failure even though Turnstile would have happily issued a token.

fix(unified-startMint): Extract a single `startMint()` that both takeToken (cold path) and preMintToken share. Concurrent calls return the same in-flight promise, so only one `turnstile.execute()` ever runs and the supersede branch only triggers when there is genuinely a stale request.

fix(takeToken-abort-race): The signal threading is now via a `waitWithAbort` helper that races the awaiter against the abort signal but lets the underlying mint keep going. If the user cancels mid-mint, the underlying challenge still runs to completion and the token lands in the cache for whoever submits next, instead of being thrown away.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:32:09 +09:00
Kazuki Yamada 7c9e44616b perf(website): Pre-mint Turnstile token on user intent, not on mount
intent(latency-and-counter): The Cloudflare Turnstile dashboard's "challenges issued" counter sits at roughly 1.25× of GA page_view, which means simply loading `api.js` on every visitor (the PR #1541 pre-warm) is already inflating the counter — render() and execute() are not the only trigger. At the same time, click→token latency is the main remaining UX cost. This PR reshapes pre-warm so script load + challenge happen only when the user has shown real intent (filled a valid URL or chose a file *and* interacted with the form), achieving both a lower counter and a near-zero click→token latency.

fix(useTurnstile-api): Replace the single `getToken()` entry point with a `preMintToken()` / `takeToken()` pair backed by an in-memory `{ token, mintedAt, consumed }` cache. `preMintToken()` runs the challenge in the background and stashes the resulting token; `takeToken()` consumes the cache synchronously (instant submit) or awaits the in-flight mint, falling back to a cold mint with the supplied AbortSignal. `invalidateCache()` lets the caller drop the cache without minting a new token. TTL is bounded at 240 s — Cloudflare hard-caps tokens at 300 s, the margin absorbs clock skew and network round-trips so a token that's "almost expired" is never sent to siteverify.

fix(useTurnstile-no-mount-prewarm): Stop calling `loadTurnstileScript()` from `setContainer()`. The mount-time script load was the source of the page-view-shaped counter inflation. Pre-warm now only runs from the intent-gated trigger in `usePackRequest`, so visitors who never interact with the form never appear on the dashboard.

fix(usePackRequest-intent-gate): Add a `userTouched` ref that flips on real user interaction (input event, file upload, mode switch) and never goes back. A debounced (500 ms) `watch(isSubmitValid && userTouched)` calls `preMintToken()`, so URL-parameter hydration (`?repo=`), browser form restoration, and autofill never pre-mint. `submitRequest()` switches from `getToken()` to `takeToken()` so the cached token is consumed on the first click, with the cold mint path as a transparent fallback.

fix(TryItUrlInput-user-input-event): Emit a new `userInput` event from the URL field's actual `@input` handler. `TryIt.vue` wires it to `markUserTouched()`. Watching `inputUrl` directly would have re-fired during onMounted hydration and defeated the gate.

learned(cloudflare-counter-includes-script-load): Even with `execution: 'execute'`, the Cloudflare Turnstile dashboard counts api.js loads toward "challenges issued" (verified by comparing GA page_view ≈ 106 with CF issued ≈ 132 in the same 30 min window after the PR #1541 deploy). Treat any new place that loads api.js as a billable analytics side effect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:16:54 +09:00
Kazuki Yamada 0f3a6e69a1 fix(website): Address review feedback on PR #1541
intent(comment-drift): Claude's review surfaced three stale comments that no longer match the code after pre-warm was narrowed to the script-load step. No behaviour change — the comments were lying about what the code actually does now.

fix(useTurnstileScript-jsdoc): The JSDoc on `TurnstileRenderOptions.execution` claimed `'execute'` was chosen so the pre-warm path could render() without firing a challenge. PR #1541 proved that's not what `'execute'` does in practice — render() itself counts toward the dashboard's challenge counters, regardless of `execution`. Rewrote the comment to explain why we still pass `'execute'` (token-mint guardrail) and why we no longer pre-warm by rendering.

fix(useTurnstile-render-comment): The inline comment at the render() call site said this option is "what makes the pre-warm in setContainer() free of side-effects". setContainer() no longer calls render(), so the rationale is obsolete. Updated to describe the current role: a guardrail against an accidental render() minting a token before getToken() is ready.

fix(useTurnstile-race-comment): The single-flight cache comment said "pre-warm and getToken() can both race past the widgetId.value null check". Pre-warm doesn't enter ensureWidget anymore, so only back-to-back getToken() submits can race. Updated to reflect the narrower scope.

perf(preconnect-crossorigin): Add a `crossorigin` companion to the existing `<link rel="preconnect">`. Turnstile's `api.js` is fetched anonymously, but the Turnstile iframe issues CORS sub-requests on a separate browser connection pool. Without both hints, the iframe's first handshake still happens on click. Cheap defensive addition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 15:37:59 +09:00
Kazuki Yamada c5fd291399 fix(website): Defer Turnstile render() until pack click
intent(token-waste): Production telemetry (12-hour Cloudflare dashboard sample) showed 2,620 challenges issued and 986 solved against only ~150-200 actual pack clicks tracked in GA. Despite the widget being configured with `execution: 'execute'`, calling `turnstile.render()` at form-mount time was inflating the dashboard's challenge counters by every visitor — humans, crawlers, ad-blocked browsers, abandoned tabs. Cloudflare's docs say render() should be side-effect free in execute mode, but the analytics disagree.

fix(prewarm-scope): Drop the widget render from `setContainer()`. Pre-warm now only calls `loadTurnstileScript()` so the script is cached before the user clicks; the actual `turnstile.render()` happens on the first `getToken()` call. This restores the documented 1:1 relationship between solved challenges and actual pack submissions.

perf(preconnect): Add `preconnect` and `dns-prefetch` hints to challenges.cloudflare.com so the DNS / TLS / HTTP/2 handshake is warm before the click. Compensates for losing the render() pre-warm — the script load and the challenge round-trip both reuse the warmed connection.

learned(don't-trust-docs-when-telemetry-disagrees): When the dashboard says one thing and the docs say another, the dashboard wins. The previous PR added pre-warm believing render() was inert in execute mode; the analytics showed it wasn't. Going forward, treat any new render() call site as a billable side effect until proved otherwise on the dashboard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 14:38:41 +09:00
Kazuki Yamada 7fdda0e897 fix(website): Single-flight ensureWidget to prevent double render race
intent(race-fix): gemini レビューで指摘 — pre-warm 導入により ensureWidget が `setContainer` (pre-warm) と `getToken` (submit) の両方から並行に呼ばれる経路ができた。両者が `await loadTurnstileScript()` で待機 → 解決後に両方が `if (!widgetId.value)` を通過し `turnstile.render()` を 2 回実行 → 最初の widgetId が上書きされ leak (onBeforeUnmount の remove() は 2 個目しか掃除できない)。

fix(single-flight): module-level ではなく composable-instance スコープの `ensureWidgetPromise` で in-flight render を 1 件に絞る。後続呼び出しは同じ promise を返すので render() は最大 1 回。失敗時は promise を null にして retry 経路を維持 (useTurnstileScript の resetForRetry 由来)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 13:23:12 +09:00
Kazuki Yamada cbf41ba9a7 fix(website): Clear Turnstile containerEl on unmount
intent(race-safety): codex re-review で指摘 — pre-warm 中に component が unmount された場合、ensureWidget の script load Promise が後から戻ってきて detached DOM element に widget を render してしまう余地がある。既存の `containerEl.value !== el` ガードはあるが、それを成立させるには unmount 時に containerEl を解放する必要がある。
fix(unmount-cleanup): onBeforeUnmount 冒頭で `containerEl.value = null` を明示。これで in-flight pre-warm が後から戻っても `null !== el` で render を skip し、widget が detached node にバインドされて remove() がない状態で leak することを防ぐ。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 13:14:11 +09:00
Kazuki Yamada 0e0f572964 fix(website): Defer Turnstile challenge with execution='execute'
intent(token-waste): デプロイ後の Turnstile ダッシュボードで「解決チャレンジ 90 / siteverify 22」というギャップ (約 68 個の token が無駄に発行されて捨てられている) と「未解決チャレンジ 127 件 / ボット可能性 58.53%」が観測された。

fix(root-cause): Cloudflare Turnstile の invisible widget はデフォルトで `execution: 'render'` モードであり、`render()` 直後に automatic challenge を実行して token を発行する。我々の pre-warm ロジック (commit `68090aa2`) は render() で widget を立ち上げると同時に challenge も走らせていた。pack ボタンを押すと execute() で 2 度目の challenge が走り、最初の token は捨てられる。さらに正規クローラ (Googlebot 等) がホームページを訪れた際、JS 実行で widget が render → automatic challenge → クローラは解けず「未解決」としてカウント、これがボット率を inflate していた。

fix(execution-execute): render オプションに `execution: 'execute'` を追加。これで pre-warm は script load + widget shell のみ実行し、challenge は明示的な turnstile.execute() (= ユーザが pack ボタン押下時) まで遅延する。pre-warm の latency 削減効果はそのまま、token 浪費とクローラ false-positive は消える。

constraint(type-update): TurnstileRenderOptions interface に `execution?: 'render' | 'execute'` を追加して TypeScript で表現可能に。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 13:09:42 +09:00
Kazuki Yamada 68090aa2d2 perf(website): Pre-warm Turnstile widget on container mount
intent(latency): Turnstile 導入後の "Processing repository..." 直後の体感ラグ (~1s) を削減。デプロイ後の動作確認で、pack ボタン押下から実 API 呼び出しまでに体感 1 秒のスピナ点滅が観測された。原因は click 時にゼロから「script load → widget init → execute → token」を直列で走らせていたこと。
fix(prewarm): setContainer で element が登録された時点で ensureWidget を fire-and-forget で発火。script load + widget init をページの idle 時間に前倒し、click 時は execute() のみで済むようにする。invisible widget は token を勝手に発行しないので prewarm しても token を浪費しない。
constraint(error-swallow): pre-warm 失敗は意図的に握り潰す。ページレンダリングをブロックさせない方が良く、同じ loadTurnstileScript / ensureWidget の経路は getToken() 呼び出し時に再実行される(リトライ + フルエラー伝搬)ので、ユーザーには submit 時に正しく見える。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 13:07:03 +09:00
Kazuki Yamada b58644154e fix(website): Address claude iteration 2 review
intent(maintainability): claude のフォローアップレビューで残った Improve / test gap / nit を対応。production 影響はないが将来の保守性を上げる。

fix(server/log-helper): turnstile.ts で 7 回繰り返されていた `{ event, outcome, reason, requestId, source, ...cf }` の log payload 構築を `rejectAndLog(reason, message, level, extra)` ヘルパーにまとめた。約 40 行短縮、新しい reason を追加するときに envelope を取りこぼすリスクが消える。
fix(server/extract-siteverify): 同ファイル内に `runSiteverify` を抽出して try/catch を内包。中身が無関係な fail-closed 経路 (network error, JSON parse error) を Error sentinel として返し、middleware 本体はポリシー(reject vs pass)だけに集中させる。
fix(client/cli-fallback): script-blocked エラー時のメッセージに `npx repomix --remote owner/repo` の CLI 案内を追加。privacy extension で詰まったユーザーへの逃げ道を提示。
fix(client/file-size): useTurnstile.ts が 286 行 → 250 行ガイドライン超過していたため、script load 部分を `useTurnstileScript.ts` に分離。useTurnstile 側は widget lifecycle / token request / abort に集中。

test(turnstile): 18 → 20 cases。
- siteverify が non-JSON (HTML 5xx ページ等) を返した時に runSiteverify が Error sentinel に変換し、middleware が 403 fail-closed する経路をカバー
- cross-stack contract: server の EXPECTED_TURNSTILE_ACTION (export) と client の useTurnstile.ts に埋め込まれた `action: 'pack'` 文字列が一致する、を regex で検証。片側 rename で他方が silent break する drift を防ぐ

skipped (with reason):
- Log sampling (claude #1): 現状スケール (21K req/day = 0.24 req/sec) で Cloud Logging quota 影響なし、metric 移行のときに併せて検討
- secretMissingLogged tautology (claude #2): 既に `0b09cf9d` で logger spy に置き換え済み
- hostname check (claude #3): 既に `0b09cf9d` で ALLOWED_HOSTNAMES として実装済み
- siteverify timeout fake-timer test: codex も round 2 で skip 推奨、setup が複雑で得る保証が小さい
- Client useTurnstile.ts component test: Vue test 環境が未整備
- External /api/pack consumer release note: code change ではないので別途
- secret_missing alert: out of scope の follow-up issue

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 01:15:51 +09:00
Kazuki Yamada c5406507a4 fix(website): Remove runtime throw scoped too broadly
intent(deployment-safety): devin が指摘 — `useTurnstile.ts:139` の `import.meta.env.PROD` runtime throw が広すぎて、CF Pages preview deploy / 手元の docs:build / CI build を crash させる。これらは `.vitepress/config.ts` のコメントで「test sitekey で fall through する」と明記された環境。
fix(scope): runtime throw を削除。Production-only 保護は build-time の `.vitepress/config.ts` (CF_PAGES === '1' && CF_PAGES_BRANCH === 'main') に集約。defense-in-depth として server-side middleware の fail-closed (action/hostname mismatch で 403) が残るので、万が一 production に test sitekey が混入しても全 pack が落ちて検知できる。

learned(env-flags): import.meta.env.PROD は「すべての vitepress build 出力」で true。CF production の判定は CF_PAGES + CF_PAGES_BRANCH を見る必要がある。runtime と build-time で同じ条件式を使い分ける時は、両方で同じ env signal を参照するか、片方に集約する。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 00:54:39 +09:00
Kazuki Yamada 365732bb89 fix(website): Address codex re-review (round 3)
intent(robustness): codex round 3 で残った should-fix と nice-to-have を全て対応。

fix(client/cancel-warning): cancelRequest が requestController を null にすることで、isCurrent() ガードが現在の request の onAbort callback も stale 扱いにしていた。結果として手動 cancel のメッセージが表示されない regression が発生。cancelRequest 側で error.value を直接セットする方針に修正(isCurrent ガードは superseded request のみを止める意図通りの動作)。
fix(client/ensure-widget-detached): ensureWidget で script load 完了後に containerEl.value !== el の場合は throw。component が unmount された後に detached element に対して render() してリーク、を防ぐ。
fix(client/build-time-validation): vitepress build は SSR throw を catch して exit 0 で抜けるため、useTurnstile の throw だけでは misconfig をビルド失敗させられなかった。.vitepress/config.ts のトップレベルで production && !VITE_TURNSTILE_SITE_KEY を検出して throw、Cloudflare Pages のビルドステップが即落ちるようにする。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 23:26:16 +09:00
Kazuki Yamada 0b09cf9dfa fix(website): Address codex re-review (round 2)
intent(robustness): codex の再レビューで残った blocker と should-fix を全て対応。

fix(client/stale-callback): submitRequest の onSuccess/onError/onAbort/onProgress と getToken catch ブロック内の state mutation 全てを `isCurrent()` (= `requestController === controller`) でガード。連打で新 request が始まった後、旧 request の callback が新 request の loading/result/error を上書きする race を解消。
fix(client/abort-during-script-load): getToken に signal が渡された場合、ensureWidget() を AbortSignal-aware な Promise.race に変更。script load 中の cancel/timeout が即座に拾える。listener 登録直前にも signal.aborted を再チェック。
fix(client/site-key-throw): production で VITE_TURNSTILE_SITE_KEY 未設定なら useTurnstile() で throw。前は console.error だけだったので smoke test で見落とし得たが、throw にすればフォーム初期化が即失敗して misconfig が unmissable になる。
fix(server/hostname-claim): siteverify レスポンスの hostname を ALLOWED_HOSTNAMES (`['repomix.com']`) と照合。leaked sitekey が攻撃者ドメインで使われた場合に弾く。test sitekey は hostname を返さないので undefined は backward-compat で許可(action 検証と同じパターン)。
fix(client/window-callback-cleanup): READY_CALLBACK が success path でも `delete` されるように。stale closure 残存を完全に排除。

test(turnstile): 14 → 18 cases に拡張。
- secretMissingLogged を logger spy で「dev/test では 1 回、production では毎回」を直接 assert に変更(前は expect(true) のプレースホルダだった)
- hostname allowed / mismatch / omitted (backward-compat) を追加

skipped (codex 認定): middleware reorder。codex も「siteverify 外部呼び出しを増やすリスクがあるので見送り妥当」と再確認。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 23:19:00 +09:00
Kazuki Yamada e96835a0ad fix(website): Address claude review feedback on Turnstile
intent(robustness): claude のレビューで追加指摘が複数あったので、cancel UX と script-load 失敗時の冗長性を改善する。

fix(client/abort-signal): getToken に AbortSignal を追加。usePackRequest が controller.signal を渡すことで、cancel/timeout 中の Turnstile challenge が即座に中断される。前は getToken が hung している間(最大 15s)cancel が反映されない UX gap があった。
fix(client/onload-cleanup): resetForRetry で window[READY_CALLBACK] も delete する belt-and-suspenders。late-arriving script load が stale closure を解決する事故を排除。
fix(client/error-message): production で getToken 失敗時のエラーメッセージを 2 種類に分岐。"script/load/missing" を含むエラーは「ad blocker / privacy extension を無効化してリロード」、それ以外は generic verification failure。recovery path を提示してサポート負荷を減らす。
fix(client/cancel-during-getToken): getToken が abort で reject された場合、cancel reason に応じて「Request was cancelled」「Request timed out」を表示。短絡で handlePackRequest をスキップしてもメッセージは一致させる。

test(turnstile): remoteip omission/inclusion・secretMissingLogged 連続呼出・error-codes 配列で middleware が壊れない、を追加。10 → 14 cases。

skipped (low ROI):
- verifyResult の zod validation: Cloudflare API は安定、過剰防衛
- hostname の cross-check: Site Key が Cloudflare 側で hostname-pinned 済み
- siteverify timeout の fake-timer test: 設定が複雑な割に得られる保証が小さい

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 23:02:35 +09:00
Kazuki Yamada 3c07e5a424 fix(website): Harden Turnstile integration per codex review
intent(robustness): codex の追加レビューで複数の race condition / セキュリティ穴 / config bug が指摘されたため、production 適用前に潰す。

fix(client/timer-leak): useTurnstile の Promise.race timeout が clearTimeout されておらず、stale timer が次の getToken の pendingResolve/pendingReject を消す race を解消。generation counter (currentGen) を導入し、callback / timeout / supersede 全てを gen 検査でガード。
fix(client/finally-race): submitRequest の finally が後続リクエスト中の共有状態 (loading, requestController) を上書きする問題を解消。`if (requestController === controller)` で local controller が現役のときだけ reset。
fix(server/prod-fail-closed): production で TURNSTILE_SECRET_KEY 未設定のときは fail-open ではなく 403 fail-closed (`reason: secret_missing`)。dev/test は従来通り skip。`isProduction` を deps として注入することでテスト容易性を確保。
fix(server/action-claim): siteverify の `action` claim を `pack` と一致するか verify。token を将来の別エンドポイントから replay されないようにする。client widget 側に `data-action='pack'` を付与済み。Cloudflare のテストキーは action を返さないため undefined は backward-compat で許可。
fix(server/token-validation): token を trim、空白 only / 2048 文字超を siteverify 前に 403。Cloudflare の上限を超える token は guaranteed-invalid なので siteverify 呼ばずに弾く。
fix(client/prod-fallback-warn): VITE_TURNSTILE_SITE_KEY 未設定の production ビルドで console.error。test sitekey が本番に混入したまま deploy される事故を smoke test で気づけるようにする。
fix(client/prod-fail-fast): production で getToken 失敗時は /api/pack を呼ばずユーザー向けエラー表示。サーバ 403 確定なので origin hit を節約。dev/preview では従来通り token なしで続行。

rejected(middleware-reorder): codex は Turnstile を rate limit *前* に置けば token 無し bot が Upstash daily limit を消費しない、と指摘していたが、middleware の登録順を変える影響範囲が大きく、token max length pre-check で大半の cheap reject は実現できているため見送り。

test(turnstile): production fail-closed / whitespace token / oversized token / action mismatch / action absent (backward-compat) を追加。10 cases pass。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 22:57:47 +09:00
Kazuki Yamada 9d430f7923 fix(website): Address PR review feedback on Turnstile integration
intent(robustness): PR #1538 のレビューで複数の resilience / race condition の指摘を受けたため、Turnstile 統合の堅牢性を上げる。
fix(client/race): submitRequest 内で requestController を local const にキャプチャ。await turnstile.getToken() 中に cancelRequest() が走ると requestController=null になり、後続の requestController.signal アクセスで TypeError が出ていた (devin 指摘)。
fix(client/script-retry): scriptPromise を rejection 時に reset し、dead <script> タグを DOM から除去。CDN blip で 1 回失敗するとセッション中ずっと getToken が永久に rejection を返す問題を解消 (coderabbit 指摘)。
fix(client/hang): getToken に 15s の Promise.race timeout 追加。Cloudflare の timeout-callback は invisible widget では発火しないため、CDN stall 時にローディングスピナが永久に止まらない問題を解消 (coderabbit 指摘)。
fix(client/unmount): onBeforeUnmount で pending getToken を reject。コンポーネント破棄後も await 側にハングしていた問題を解消 (gemini 指摘)。
fix(server/remoteip): clientInfo.ip が '0.0.0.0' (=IP 取得失敗時の sentinel) の場合は siteverify の remoteip フィールドをオミット。Cloudflare の risk scoring を混乱させない (gemini 指摘)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 22:27:35 +09:00
Kazuki Yamada 523fb07111 feat(website): Add Cloudflare Turnstile verification to /api/pack
intent(pack-defense): Repomix を「コード抽出 API」として大量に叩く匿名クローラ対策。GA + Cloud Run logs の調査で 2026-04 末に 21K+ unique GitHub repos を 7 日で系統列挙されたインシデントが確認された。IP ベースの rate limit (3/min, 30/day) は機能していたが residential proxy + Tencent Cloud SG で 2,256+ unique IPs に分散され実質無力化されていた。
decision(turnstile-vs-asn): Turnstile (JS challenge) を採用。ASN ブロックは residential proxy で無力、daily limit 強化は IP 数で水増し可能。invisible JS challenge は residential proxy 経由でも通せないので一番 cost asymmetric。
constraint(scope): /api/pack のみ。docs / health / ホームページ閲覧は無関係 — Googlebot / GPTBot / ClaudeBot 等は GET HTML しか叩かないので SEO/LLMO に影響なし。
decision(fail-policy): TURNSTILE_SECRET_KEY 未設定なら fail-open(dev/preview を壊さない、警告ログは出す)。設定済みでトークン欠落・siteverify 失敗・ネットワーク失敗は全て fail-closed の 403。
decision(token-transport): X-Turnstile-Token ヘッダで送信。FormData フィールドにすると packRequestSchema (valibot) を汚染するため、cross-cutting concern として layer を分離。
decision(client-widget): invisible 不可視ウィジェットを TryIt.vue マウント時にレンダ、submit 直前に turnstile.execute() で 1-shot トークン取得。トークンは 5 分有効・1 回限りなので毎 pack で reset → execute。
rejected(form-field-token): cfTurnstileToken を FormData に入れる案 — packRequestSchema が strict object のため新フィールド追加が必要、ビジネスロジックと認証が混ざる。
rejected(asn-block): Tencent Cloud SG (AS132203) WAF ブロック — バルク部分には効くが residential proxy 部分(家庭 ISP・モバイル・大学ネット)が世界中に散らばっており ASN 単位で弾けない、正規ユーザを巻き込むリスク。
rejected(daily-limit-tightening): 30 → 10/day per IP — IP 数で水増しできる相手には無意味、人間ユーザの体験のみ悪化。
constraint(observability): outcome="turnstile_failed" として既存の pack_completed イベントに乗せる。新 metric 不要、既存ダッシュボードに自動で reject reason として現れる。
learned(rate-limit-effectiveness): スパイク期間中の SG pack 成功率は 0.15% (32/21,501)。app-level rate limit は処理は止めていたが入口の負荷(TCP/TLS/Upstash check)は受けていた。Turnstile は CDN 層に近く、より早く弾ける利点がある。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:41:04 +09:00
Kazuki Yamada 902f87d353 feat(website): Stream pack progress via NDJSON
Add real-time progress streaming to the pack endpoint using Hono's
stream() helper with NDJSON format. Users now see stage-specific
messages during processing instead of a static "Processing repository..."

Server changes:
- packAction uses Hono stream() with NDJSON (one JSON per line)
- processRemoteRepo split into git clone + runDefaultAction for
  separate cloning/processing stages
- processZipFile reports extracting/processing stages
- Content-Encoding: identity skips compress for real-time delivery

Client changes:
- packRepository parses NDJSON stream with onProgress callback
- TryItLoading displays stage messages (Checking cache, Cloning
  repository, Processing files, etc.)
- message field prepared for future detailed progress from pack()

Progress stages:
- URL: cache-check → cloning → processing → result
- ZIP: extracting → processing → result

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 22:08:48 +09:00
Yamada Dev cf9e7c90e6 refactor(core): Inline .bind() calls in setTimeout/setInterval
Address review feedback by inlining the bound abort functions directly
into setTimeout calls instead of using intermediate variables.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 23:28:19 +09:00
Yamada Dev a1306f14be fix(website): Prevent closure memory leaks in setTimeout and setInterval
- usePackRequest.ts: Use `controller.abort.bind(controller, 'timeout')`
  instead of an arrow function to avoid capturing the surrounding scope
- cache.ts: Save setInterval ID for cleanup, use `.bind()` instead of
  arrow function, add `.unref()` to not block process exit, and add
  `dispose()` method for proper resource cleanup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 23:28:19 +09:00
Kazuki Yamada d9e141ec65 feat(website): add cancel functionality for pack requests
Implement cancel button for pack requests with improved user experience.
The button now shows "Cancel" on hover during processing and allows users
to abort ongoing requests. Also improved error messages for better clarity.

- Add cancel functionality to PackButton with hover state
- Implement request cancellation using AbortController
- Improve error messages for remote repository processing failures
- Update timeout handling and server configuration
- Add proper event handling to prevent form submission conflicts
2025-09-23 19:52:10 +09:00
Kazuki Yamada b6a4174c93 feat(website): improve timeout handling with warning display
- Add onAbort callback to distinguish timeouts from errors
- Implement errorType system to differentiate warnings from errors
- Update TryItResultErrorContent to show warning styling for timeouts
- Change timeout message to suggest using Include/Ignore Patterns
- Timeout now displays with yellow warning icon instead of red error
2025-09-23 18:37:57 +09:00
Kazuki Yamada b7fe6f25c5 fix(lint): resolve all oxlint warnings for code quality
- Remove unused imports across 67 files (RepomixConfigMerged, QueryCapture, etc.)
- Fix unused parameters by prefixing with underscore (_context, _index, etc.)
- Remove unused catch parameters using modern JavaScript syntax
- Fix require-yield warnings in generator functions
- Remove unused variables and interface declarations
- Add oxlint configuration to ignore integration test fixtures

Resolves 144 linting warnings while preserving all functionality.
All 743 tests continue to pass. Code quality significantly improved.
2025-08-24 18:25:08 +09:00
Kazuki Yamada cc540eed3a fix: lint 2025-08-24 16:41:33 +09:00
Kazuki Yamada 7ff2e00715 fix: lint 2025-08-24 16:29:46 +09:00
Kazuki Yamada 2ddcaafb33 style(website): fix linting issues and improve code formatting
- Replace forEach loops with for...of loops for better performance
- Fix import ordering and spacing inconsistencies
- Simplify interface definitions and type declarations
- Add proper padding to tokens column in file selection table
- Remove unnecessary line breaks in computed properties
- Improve code readability and consistency across components
2025-08-24 15:39:04 +09:00
Kazuki Yamada f7c14f64b8 feat(website): improve file selection UI and functionality
- Convert file list to table format with clickable rows and compact layout
- Add warning message when selecting more than 500 files
- Increase include patterns limit from 1,000 to 100,000 characters
- Auto-switch to Result tab when Re-pack button is clicked
- Fix re-pack loading state to use shared loading status
- Sort files by token count instead of character count
- Replace Package icon with custom SVG for re-pack button
- Add row hover effects and full-row click selection
- Implement proper file selection state management during re-pack
2025-08-24 15:27:07 +09:00
spandan-kumar 27626d42dd feat(website): add file selection checkboxes for selective re-packing
- Add FileInfo interface for individual file metadata
- Extend PackResult to include allFiles array with complete file information
- Create TryItFileSelection component with checkboxes for each file
- Add bulk selection controls (Select All/Deselect All)
- Implement re-packing functionality for selected files only
- Add live statistics showing selected files and token counts
- Fix TypeScript configuration for proper import.meta.env support
- Add responsive design for mobile and desktop
- Include scrollable file list with proper overflow handling

Resolves the GitHub issue requesting checkboxes for file inclusion/exclusion
on the website UI. Provides tree-based selection interface after initial
packing process as suggested by maintainer.

# Conflicts:
#	website/client/components/Home/TryItResult.vue
2025-08-24 13:02:50 +09:00
Kazuki Yamada 0fb312be3c refactor(website): Optimize URL parameter parsing and address review feedback
Centralize URL parameter parsing to improve efficiency and maintainability based on code review feedback.

Changes:
- Move URL parameter parsing to single location in usePackRequest onMounted
- Add applyUrlParameters function to usePackOptions for external use
- Remove duplicate parseUrlParameters calls (was called twice)
- Add detailed comments explaining SSR/hydration timing requirements
- Clean up imports in usePackOptions

This addresses review feedback from Gemini and Copilot while maintaining the SSR/hydration fix functionality.
2025-08-17 20:51:59 +09:00
Kazuki Yamada 1be8b8fdfd fix(website): Fix URL parameter initialization timing for production builds
URL parameters were not properly applied in production builds due to timing issues with SSR/hydration. This change moves URL parameter application to onMounted lifecycle to ensure proper initialization after the component is fully mounted on the client side.

Changes:
- Move URL parameter parsing from initialization to onMounted in usePackOptions
- Move repo parameter application to onMounted in usePackRequest
- Remove debug console logs
- Fix import statements for onMounted

This resolves the issue where format=markdown and other URL parameters worked in development but not in production builds.
2025-08-17 20:22:34 +09:00
Kazuki Yamada 853be1f776 fix(website): resolve lint errors and revert tooltip styling
- Replace 'any' with 'unknown' for better type safety
- Convert forEach to for...of loop for better performance
- Use proper type casting with unknown intermediate step
- Revert tooltip colors back to original styling (#333 background, white text)
- Fix all biome lint warnings

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-17 14:50:11 +09:00
Kazuki Yamada 6f7de896b3 fix(website): address PR review feedback
- Simplify shouldShowReset string comparison logic
- Use CSS variables for tooltip colors instead of hardcoded values
- Improve type safety in URL parameter parsing with proper type casting
- Remove redundant conditional in pack option comparison

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-17 14:38:31 +09:00
Kazuki Yamada 9911ad9dd9 feat(website): add URL query string parameters support for web interface
Implement comprehensive URL query parameter functionality for Repomix web interface
to enable easy sharing and bookmarking of configurations.

Features:
- Parse URL parameters on page load and apply settings automatically
- Update URL only when Pack button is pressed (not real-time)
- Exclude default values from URL parameters to keep URLs clean
- Reset functionality with icon-only button next to Pack button
- Hover tooltip for reset button explaining functionality
- Support for all pack options: format, patterns, boolean flags
- Auto-pack when valid repository URL is provided via parameters

Changes:
- Add urlParams.ts utility for parsing and updating URL parameters
- Update usePackOptions to initialize from URL parameters
- Modify usePackRequest to handle URL parameter logic
- Add reset button with tooltip next to Pack button (appears after Pack execution)
- Remove previous reset button from options panel
- Implement smart URL generation (only non-default values included)

Examples:
- Basic: ?repo=https://github.com/user/repo&format=markdown
- Advanced: ?repo=https://github.com/user/repo&include=**/*.cs&compress=true

Resolves #764

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-17 13:48:46 +09:00
Kazuki Yamada cbd404ed70 refactor(website): Replace JSZip with fflate for improved performance
User requested replacing JSZip with fflate in website/client:
- User asked: "website/clientでjszipではなくfflateを使うようにしてください。"
- Migrated from JSZip to fflate for smaller bundle size and better performance
- Updated useZipProcessor.ts to use fflate's functional API instead of JSZip's class-based API
- Maintained same functionality while improving efficiency

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-08 19:41:03 +09:00
Kazuki Yamada 135e5352c2 fix(website): Add safety limits for recursive directory traversal to prevent performance issues 2025-06-08 14:02:01 +09:00
Kazuki Yamada 03b6b1ea73 fix(website): Add error handling for multiple files without preprocessor 2025-06-08 13:58:21 +09:00
Kazuki Yamada d300aaf5f3 refactor(website): Extract default pack options to eliminate duplication 2025-06-08 13:56:48 +09:00
Kazuki Yamada 25a208b81d fix(website): Clear file input after selection to prevent re-selection issues 2025-06-08 13:53:25 +09:00
Kazuki Yamada 8694df19aa fix(website): Remove throw error in setTimeout callback to prevent unhandled promise rejection 2025-06-08 13:50:01 +09:00
Kazuki Yamada 968460edbc refactor(website/client): Extract upload logic into shared composables
- Create useFileUpload composable for unified drag/drop, validation, and file processing
- Create useZipProcessor composable for ZIP file operations
- Refactor TryItFileUpload.vue: reduce from 213 to 105 lines (50% reduction)
- Refactor TryItFolderUpload.vue: reduce from 342 to 115 lines (66% reduction)
- Eliminate code duplication while maintaining all existing functionality
- Improve type safety with configurable validation and preprocessing pipelines
- Enable better reusability and testability of upload logic

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-08 13:35:37 +09:00