mirror of
https://github.com/yamadashy/repomix.git
synced 2026-10-13 22:49:21 +02:00
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>
198 lines
4.8 KiB
Vue
198 lines
4.8 KiB
Vue
<script setup lang="ts">
|
|
import { AlertTriangle } from 'lucide-vue-next';
|
|
import { computed, onMounted, ref } from 'vue';
|
|
import { isValidRemoteValue } from '../utils/validation';
|
|
import PackButton from './PackButton.vue';
|
|
|
|
const props = defineProps<{
|
|
url: string;
|
|
loading: boolean;
|
|
showButton?: boolean;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
'update:url': [value: string];
|
|
submit: [];
|
|
keydown: [event: KeyboardEvent];
|
|
cancel: [];
|
|
// Emitted on real DOM input — typing, paste, IME compose end, datalist
|
|
// selection. Used by usePackRequest to gate the Turnstile pre-mint so
|
|
// URL-parameter hydration / form restoration don't trigger background
|
|
// challenges.
|
|
userInput: [];
|
|
}>();
|
|
|
|
const isValidUrl = computed(() => {
|
|
if (!props.url) return false;
|
|
return isValidRemoteValue(props.url.trim());
|
|
});
|
|
|
|
// Array to manage input history
|
|
const urlHistory = ref<string[]>([]);
|
|
const historyListId = 'repository-url-history';
|
|
|
|
// Load URL history when component is mounted
|
|
onMounted(() => {
|
|
loadUrlHistory();
|
|
});
|
|
|
|
// Load URL history from localStorage
|
|
function loadUrlHistory() {
|
|
try {
|
|
const savedHistory = localStorage.getItem('repomix-url-history');
|
|
if (savedHistory) {
|
|
urlHistory.value = JSON.parse(savedHistory);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load URL history from localStorage:', error);
|
|
// Continue with empty history rather than displaying an error to the user
|
|
// as this is a non-critical feature
|
|
urlHistory.value = [];
|
|
}
|
|
}
|
|
|
|
// Save URL to history
|
|
function saveUrlToHistory(url: string) {
|
|
if (!url) return;
|
|
|
|
const trimmedUrl = url.trim();
|
|
if (!isValidRemoteValue(trimmedUrl)) return;
|
|
|
|
// Remove existing entry and add to the beginning
|
|
const filteredHistory = urlHistory.value.filter((item) => item !== trimmedUrl);
|
|
urlHistory.value = [trimmedUrl, ...filteredHistory].slice(0, 5); // Keep only the latest 10 entries
|
|
|
|
try {
|
|
localStorage.setItem('repomix-url-history', JSON.stringify(urlHistory.value));
|
|
} catch (error) {
|
|
console.error('Failed to save URL history to localStorage:', error);
|
|
// Non-critical error, so we don't need to show it to the user
|
|
}
|
|
}
|
|
|
|
function handleUrlInput(event: Event) {
|
|
const input = event.target as HTMLInputElement;
|
|
emit('update:url', input.value);
|
|
emit('userInput');
|
|
}
|
|
|
|
// Process and save valid URL
|
|
function processValidUrl() {
|
|
if (isValidUrl.value) {
|
|
saveUrlToHistory(props.url);
|
|
}
|
|
}
|
|
|
|
function handleSubmit() {
|
|
processValidUrl();
|
|
emit('submit');
|
|
}
|
|
|
|
function handleKeydown(event: KeyboardEvent) {
|
|
if (event.key === 'Enter' && isValidUrl.value) {
|
|
processValidUrl();
|
|
}
|
|
emit('keydown', event);
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="input-group">
|
|
<div class="url-input-container">
|
|
<input
|
|
:value="url"
|
|
@input="handleUrlInput"
|
|
@keydown="handleKeydown"
|
|
type="text"
|
|
placeholder="GitHub repository URL or user/repo (e.g., yamadashy/repomix)"
|
|
class="repository-input"
|
|
:class="{ 'invalid': url && !isValidUrl }"
|
|
aria-label="GitHub repository URL"
|
|
autocomplete="on"
|
|
:list="historyListId"
|
|
/>
|
|
<datalist :id="historyListId">
|
|
<option v-for="historyUrl in urlHistory" :key="historyUrl" :value="historyUrl" />
|
|
</datalist>
|
|
</div>
|
|
|
|
<div v-if="url && !isValidUrl" class="url-warning">
|
|
<AlertTriangle class="warning-icon" :size="16" />
|
|
<span>Please enter a valid GitHub repository URL (e.g., yamadashy/repomix)</span>
|
|
</div>
|
|
<div v-if="showButton" class="pack-button-container">
|
|
<PackButton :isValid="isValidUrl" :loading="loading" @click="handleSubmit" @cancel="$emit('cancel')"/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.input-group {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.url-input-container {
|
|
flex: 1;
|
|
position: relative;
|
|
height: 100%;
|
|
}
|
|
|
|
.repository-input {
|
|
width: 100%;
|
|
height: 50px;
|
|
padding: 12px 16px;
|
|
font-size: 16px;
|
|
border: 1px solid var(--vp-c-border);
|
|
border-radius: 8px;
|
|
background: var(--vp-c-bg);
|
|
color: var(--vp-c-text-1);
|
|
transition: border-color 0.2s;
|
|
/* Hide datalist dropdown arrow in different browsers */
|
|
&::-webkit-calendar-picker-indicator {
|
|
display: none !important;
|
|
}
|
|
&::-webkit-list-button {
|
|
display: none !important;
|
|
}
|
|
&::-webkit-inner-spin-button {
|
|
display: none !important;
|
|
}
|
|
-moz-appearance: none;
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
}
|
|
|
|
.repository-input:focus {
|
|
outline: none;
|
|
border-color: var(--vp-c-brand-1);
|
|
}
|
|
|
|
.repository-input.invalid {
|
|
border-color: var(--vp-c-danger-1);
|
|
}
|
|
|
|
.url-warning {
|
|
margin-top: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
color: var(--vp-c-warning-1);
|
|
font-size: 14px;
|
|
}
|
|
|
|
.warning-icon {
|
|
flex-shrink: 0;
|
|
color: var(--vp-c-warning-1);
|
|
}
|
|
|
|
.pack-button-container {
|
|
margin-top: 16px;
|
|
display: flex;
|
|
justify-content: center;
|
|
width: 100%;
|
|
}
|
|
</style>
|