playwright / solver /web /templates /index.html
cv3inx's picture
Bundle turnstile-solver in the project (no external URL needed)
f26e640
Raw
History Blame Contribute Delete
22.2 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Violetics Solver</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%23111'/%3E%3Cpath d='M7 10 L16 24 L25 10 L21.5 10 L16 19 L10.5 10 Z' fill='%23b28dff'/%3E%3C/svg%3E">
<style>
:root {
--bg: #0a0a0a;
--panel: #141414;
--line: #222;
--text: #e8e8e8;
--mute: #888;
--mute-2: #555;
--accent: #b28dff;
--ok: #7ddfa0;
--err: #ff6b83;
--warn: #e8c56b;
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
* { box-sizing: border-box }
html, body { margin: 0; padding: 0; background: var(--bg); color: var(--text) }
body {
font: 13px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
min-height: 100vh;
}
a { color: var(--mute); text-decoration: none }
a:hover { color: var(--text) }
button { font: inherit; cursor: pointer }
.mono { font-family: var(--mono) }
header {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 20px;
border-bottom: 1px solid var(--line);
position: sticky; top: 0; background: var(--bg); z-index: 5;
}
header .brand { display: flex; align-items: center; gap: 10px }
header .brand strong { font-size: 14px; font-weight: 600 }
header .brand span { font: 11px var(--mono); color: var(--mute) }
header .links { display: flex; gap: 6px; align-items: center }
.pill {
border: 1px solid var(--line); color: var(--mute);
padding: 4px 10px; border-radius: 4px;
font: 11px var(--mono);
}
.pill:hover { color: var(--text); border-color: var(--mute-2) }
.metrics {
display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
border-bottom: 1px solid var(--line);
}
.metric {
padding: 10px 16px;
border-right: 1px solid var(--line);
}
.metric:last-child { border-right: 0 }
.metric .lbl {
font-size: 10px; color: var(--mute);
text-transform: uppercase; letter-spacing: .5px;
margin-bottom: 4px;
}
.metric .val { font: 15px var(--mono); transition: color .3s }
.metric .val.pulse { color: var(--accent) }
main {
display: grid;
grid-template-columns: minmax(300px, 1fr) minmax(360px, 1.3fr);
gap: 20px;
padding: 20px;
max-width: 1400px;
margin: 0 auto;
}
@media (max-width: 900px) { main { grid-template-columns: 1fr } }
.col { display: flex; flex-direction: column; gap: 20px; min-width: 0 }
.card {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 6px;
overflow: hidden;
}
.card > header {
position: static;
padding: 10px 14px;
border-bottom: 1px solid var(--line);
background: transparent;
display: flex; align-items: center; justify-content: space-between;
gap: 10px;
}
.card > header h2 {
margin: 0; font: 600 11px/1 inherit;
text-transform: uppercase; letter-spacing: .8px;
color: var(--mute);
}
.card-body { padding: 14px }
.tabs { display: flex; gap: 2px }
.tab {
background: transparent; border: 0;
padding: 4px 10px; border-radius: 4px;
font: 11px var(--mono); color: var(--mute);
user-select: none;
}
.tab:hover { color: var(--text) }
.tab.active { color: var(--text); background: var(--bg) }
.tab-panel { display: none }
.tab-panel.active { display: block }
.field { margin-bottom: 10px; min-width: 0 }
.field-row { display: flex; gap: 10px }
.field-row > .field { flex: 1 }
label {
display: block; margin-bottom: 4px;
font-size: 10px; letter-spacing: .5px;
text-transform: uppercase; color: var(--mute);
}
input, textarea, select {
width: 100%;
background: var(--bg); color: var(--text);
border: 1px solid var(--line); border-radius: 4px;
padding: 7px 10px;
font: 12.5px/1.4 var(--mono);
}
input:focus, textarea:focus, select:focus {
outline: none; border-color: var(--mute-2);
}
textarea { min-height: 120px; resize: vertical }
select { cursor: pointer }
.chiprow { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 2px }
.chip {
background: transparent; border: 1px solid var(--line);
border-radius: 4px; padding: 3px 9px;
font: 11px var(--mono); color: var(--mute);
cursor: pointer;
}
.chip:hover { color: var(--text); border-color: var(--mute-2) }
.actions {
display: flex; align-items: center; gap: 12px;
margin-top: 12px; padding-top: 12px;
border-top: 1px solid var(--line);
}
button.primary {
background: var(--accent); color: #15101e;
border: 0; padding: 7px 16px; border-radius: 4px;
font: 600 12px/1 inherit;
}
button.primary:hover { filter: brightness(1.1) }
button.primary:disabled { opacity: .5; cursor: not-allowed }
.status { font: 11px var(--mono); color: var(--mute); display: flex; gap: 6px; align-items: center }
.status.ok { color: var(--ok) }
.status.err { color: var(--err) }
.status.run { color: var(--warn) }
.spin {
width: 10px; height: 10px; border-radius: 50%;
border: 1.5px solid currentColor; border-top-color: transparent;
animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg) } }
.hint { margin-left: auto; font: 11px var(--mono); color: var(--mute-2) }
pre.out {
margin: 0; padding: 12px 14px;
font: 12px/1.55 var(--mono);
white-space: pre-wrap; word-break: break-all;
color: var(--text);
background: var(--bg);
max-height: 420px; overflow: auto;
}
.snippet-card pre.out { max-height: 300px }
pre.out::-webkit-scrollbar { width: 8px; height: 8px }
pre.out::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px }
.meta { display: flex; gap: 6px; align-items: center; flex-wrap: wrap }
.kv {
display: inline-flex; gap: 4px;
border: 1px solid var(--line); padding: 2px 8px; border-radius: 4px;
font: 11px var(--mono); color: var(--mute);
}
.kv b { font-weight: 500; color: var(--mute-2) }
.kv em { font-style: normal; color: var(--text) }
.event-list { display: flex; flex-direction: column; max-height: 340px; overflow: auto }
.event-list .empty { padding: 18px; text-align: center; color: var(--mute); font-size: 12px }
.event {
display: grid; grid-template-columns: 72px 140px 90px 1fr;
gap: 12px; padding: 7px 14px;
border-bottom: 1px solid var(--line);
font: 11.5px/1.4 var(--mono); align-items: center;
}
.event:last-child { border-bottom: 0 }
.event:hover { background: var(--bg) }
.event .ts { color: var(--mute-2) }
.event .st { color: var(--mute) }
.event .st.ok { color: var(--ok) }
.event .st.err { color: var(--err) }
.event .sum { color: var(--mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
footer { text-align: center; font: 11px var(--mono); color: var(--mute-2); padding: 16px }
</style>
</head>
<body>
<header>
<div class="brand">
<svg width="22" height="22" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#1a1a1a"/><path d="M7 10 L16 24 L25 10 L21.5 10 L16 19 L10.5 10 Z" fill="#b28dff"/></svg>
<div>
<strong>Violetics Solver</strong>
<div><span id="h_endpoint"></span></div>
</div>
</div>
<div class="links">
<span class="pill" id="h_mode"></span>
<a class="pill" href="/health">health</a>
<a class="pill" href="/stats">stats</a>
</div>
</header>
<section class="metrics">
<div class="metric"><div class="lbl">Uptime</div><div class="val" id="st_uptime"></div></div>
<div class="metric"><div class="lbl">In-flight</div><div class="val" id="st_inflight">0</div></div>
<div class="metric"><div class="lbl">Solved</div><div class="val" id="st_solved">0</div></div>
<div class="metric"><div class="lbl">Challenges</div><div class="val" id="st_challenges">0</div></div>
<div class="metric"><div class="lbl">Errors</div><div class="val" id="st_errors">0</div></div>
<div class="metric"><div class="lbl">Success</div><div class="val" id="st_success"></div></div>
<div class="metric"><div class="lbl">Avg</div><div class="val" id="st_avg"></div></div>
<div class="metric"><div class="lbl">p95</div><div class="val" id="st_p95"></div></div>
</section>
<main>
<div class="col">
<section class="card">
<header>
<h2>Request</h2>
<div class="tabs" data-group="request">
<button class="tab active" data-tab="solve">Turnstile</button>
<button class="tab" data-tab="challenge">CF Challenge</button>
<button class="tab" data-tab="raw">Raw JSON</button>
</div>
</header>
<div class="card-body">
<section class="tab-panel active" data-panel="solve">
<div class="field">
<label for="s_url">Site URL</label>
<input id="s_url" value="https://example.com/" spellcheck="false">
</div>
<div class="field">
<label for="s_key">Site Key</label>
<input id="s_key" value="1x00000000000000000000AA" spellcheck="false">
</div>
<div class="field-row">
<div class="field"><label for="s_to">Timeout (s)</label><input id="s_to" type="number" min="5" max="180" value="45"></div>
<div class="field"><label for="s_action">Action (opt)</label><input id="s_action" placeholder="—" spellcheck="false"></div>
</div>
<div class="chiprow">
<span class="chip" data-preset="solve" data-url="https://example.com/" data-key="1x00000000000000000000AA">test pass</span>
<span class="chip" data-preset="solve" data-url="https://example.com/" data-key="2x00000000000000000000AB">test block</span>
<span class="chip" data-preset="solve" data-url="https://www.instveed.com/en/" data-key="0x4AAAAAAC3x1HiBz5IFyj7s">instveed</span>
</div>
</section>
<section class="tab-panel" data-panel="challenge">
<div class="field">
<label for="c_url">Site URL</label>
<input id="c_url" value="https://api.sawit.biz.id/docs" spellcheck="false">
</div>
<div class="field-row">
<div class="field"><label for="c_to">Timeout (s)</label><input id="c_to" type="number" min="5" max="180" value="60"></div>
</div>
<div class="chiprow">
<span class="chip" data-preset="challenge" data-url="https://api.sawit.biz.id/docs">sawit</span>
<span class="chip" data-preset="challenge" data-url="https://cobalt.meowing.de/">cobalt</span>
<span class="chip" data-preset="challenge" data-url="https://nopecha.com/demo/cloudflare">nopecha</span>
</div>
</section>
<section class="tab-panel" data-panel="raw">
<div class="field">
<label for="r_ep">Endpoint</label>
<select id="r_ep"><option>/solve</option><option>/solve-challenge</option></select>
</div>
<div class="field">
<label for="r_body">JSON body</label>
<textarea id="r_body" spellcheck="false">{
"sitekey": "1x00000000000000000000AA",
"siteurl": "https://example.com/",
"timeout": 45
}</textarea>
</div>
</section>
<div class="actions">
<button id="run_btn" class="primary">Send</button>
<span class="status" id="run_status">ready</span>
<span class="hint" id="run_hint"></span>
</div>
</div>
</section>
<section class="card events-card">
<header>
<h2>Recent events</h2>
<span class="mono" id="ev_count" style="font-size:11px;color:var(--mute)">no events</span>
</header>
<div class="event-list" id="events">
<div class="empty">No requests yet.</div>
</div>
</section>
</div>
<div class="col">
<section class="card">
<header>
<h2>Response</h2>
<div class="meta" id="meta">
<span class="kv"><b>status</b><em></em></span>
<span class="kv"><b>time</b><em></em></span>
<button class="pill" id="copy_resp">copy</button>
</div>
</header>
<pre class="out" id="out">// response appears here</pre>
</section>
<section class="card snippet-card">
<header>
<h2>Code</h2>
<div class="tabs" data-group="snippet">
<button class="tab active" data-lang="curl">curl</button>
<button class="tab" data-lang="node">Node</button>
<button class="tab" data-lang="python">Python</button>
<button class="tab" data-lang="php">PHP</button>
<button class="tab" data-lang="go">Go</button>
<button class="pill" id="copy_snip" style="margin-left:6px">copy</button>
</div>
</header>
<pre class="out" id="snip_out">// select preset above</pre>
</section>
</div>
</main>
<footer>Violetics Solver &middot; Turnstile + CF JS-Challenge</footer>
<script>
const $ = (id) => document.getElementById(id);
const BASE = `${location.protocol}//${location.host}`;
$("h_endpoint").textContent = BASE;
document.querySelectorAll(".tabs").forEach((group) => {
const kind = group.dataset.group;
group.querySelectorAll(".tab").forEach((t) => t.addEventListener("click", () => {
group.querySelectorAll(".tab").forEach((x) => x.classList.remove("active"));
t.classList.add("active");
if (kind === "request") {
document.querySelectorAll(".tab-panel").forEach((x) => x.classList.remove("active"));
document.querySelector(`.tab-panel[data-panel="${t.dataset.tab}"]`)?.classList.add("active");
updateHint(); renderSnippet();
} else {
renderSnippet();
}
}));
});
document.querySelectorAll(".chip").forEach((c) => c.addEventListener("click", () => {
if (c.dataset.preset === "solve") {
$("s_url").value = c.dataset.url;
$("s_key").value = c.dataset.key;
} else if (c.dataset.preset === "challenge") {
$("c_url").value = c.dataset.url;
}
renderSnippet(); updateHint();
}));
function activeRequestTab() {
return document.querySelector('.tabs[data-group="request"] .tab.active')?.dataset.tab || "solve";
}
function activeLang() {
return document.querySelector('.tabs[data-group="snippet"] .tab.active')?.dataset.lang || "curl";
}
function getPayload() {
const kind = activeRequestTab();
if (kind === "solve") {
const body = {
sitekey: $("s_key").value.trim(),
siteurl: $("s_url").value.trim(),
timeout: parseInt($("s_to").value) || 45,
};
const act = $("s_action").value.trim();
if (act) body.action = act;
return { endpoint: "/solve", body };
}
if (kind === "challenge") {
return {
endpoint: "/solve-challenge",
body: {
siteurl: $("c_url").value.trim(),
timeout: parseInt($("c_to").value) || 60,
},
};
}
let body;
try { body = JSON.parse($("r_body").value); } catch { body = {}; }
return { endpoint: $("r_ep").value, body };
}
function updateHint() {
const { endpoint } = getPayload();
$("run_hint").textContent = `POST ${endpoint}`;
}
function setStatus(state, text) {
const el = $("run_status");
el.className = "status " + state;
el.innerHTML = state === "run" ? `<span class="spin"></span> ${text}` : text;
}
function setMeta(status, ms, serverMs) {
const kv = (b, e) => `<span class="kv"><b>${b}</b><em>${e}</em></span>`;
$("meta").innerHTML =
kv("status", status) +
kv("time", ms != null ? `${(ms / 1000).toFixed(2)}s` : "—") +
(serverMs != null ? kv("server", `${serverMs}s`) : "") +
`<button class="pill" id="copy_resp" onclick="copyResp()">copy</button>`;
}
window.copyResp = () => navigator.clipboard.writeText($("out").textContent).then(() => {
const btn = $("copy_resp"); if (!btn) return;
btn.textContent = "copied";
setTimeout(() => { btn.textContent = "copy"; }, 900);
});
async function send() {
const { endpoint, body } = getPayload();
if (activeRequestTab() !== "raw") {
if (!body.siteurl) { setStatus("err", "siteurl required"); return; }
if (endpoint === "/solve" && !body.sitekey) { setStatus("err", "sitekey required"); return; }
}
setStatus("run", "sending…"); $("run_btn").disabled = true;
const t0 = performance.now();
try {
const r = await fetch(BASE + endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const ct = r.headers.get("content-type") || "";
const data = ct.includes("json") ? await r.json() : await r.text();
const ms = performance.now() - t0;
setMeta(r.status, ms, data && data.elapsed);
$("out").textContent = typeof data === "string" ? data : JSON.stringify(data, null, 2);
setStatus(r.ok ? "ok" : "err", r.ok ? `ok ${r.status}` : `error ${r.status}`);
refreshStats();
} catch (e) {
setMeta("net-err", performance.now() - t0, null);
$("out").textContent = String(e);
setStatus("err", "network error");
} finally {
$("run_btn").disabled = false;
}
}
$("run_btn").onclick = send;
function renderSnippet() {
const lang = activeLang();
const { endpoint, body } = getPayload();
const url = BASE + endpoint;
const json = JSON.stringify(body, null, 2);
const compact = JSON.stringify(body);
let code = "";
if (lang === "curl") {
code = `curl -X POST ${url} \\\n -H 'Content-Type: application/json' \\\n -d '${compact}'`;
} else if (lang === "node") {
code = `const res = await fetch("${url}", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(${json}),
});
const data = await res.json();
console.log(data);`;
} else if (lang === "python") {
const py = json.replace(/: true/g, ": True").replace(/: false/g, ": False").replace(/: null/g, ": None");
code = `import requests
res = requests.post(
"${url}",
json=${py},
timeout=${(body.timeout || 45) + 15},
)
res.raise_for_status()
print(res.json())`;
} else if (lang === "php") {
code = `<?php
$ch = curl_init("${url}");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode(${phpArr(body)}),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => ${(body.timeout || 45) + 15},
]);
echo curl_exec($ch);
curl_close($ch);`;
} else if (lang === "go") {
code = `package main
import (
\t"bytes"
\t"encoding/json"
\t"fmt"
\t"io"
\t"net/http"
)
func main() {
\tbody, _ := json.Marshal(map[string]any${goMap(body)})
\tresp, err := http.Post("${url}", "application/json", bytes.NewReader(body))
\tif err != nil { panic(err) }
\tdefer resp.Body.Close()
\tout, _ := io.ReadAll(resp.Body)
\tfmt.Println(string(out))
}`;
}
$("snip_out").textContent = code;
}
function phpArr(o) {
return "[" + Object.entries(o).map(([k, v]) => `"${k}" => ${typeof v === "string" ? `"${v}"` : v}`).join(", ") + "]";
}
function goMap(o) {
return "{\n" + Object.entries(o).map(([k, v]) => `\t\t"${k}": ${typeof v === "string" ? `"${v}"` : v}`).join(",\n") + ",\n\t}";
}
$("copy_snip").onclick = () => navigator.clipboard.writeText($("snip_out").textContent).then(() => {
$("copy_snip").textContent = "copied"; setTimeout(() => { $("copy_snip").textContent = "copy"; }, 900);
});
["s_url", "s_key", "s_to", "s_action", "c_url", "c_to", "r_ep", "r_body"].forEach((id) => {
$(id).addEventListener("input", () => { renderSnippet(); updateHint(); });
$(id).addEventListener("change", () => { renderSnippet(); updateHint(); });
});
function fmtUptime(s) {
if (s < 60) return `${Math.round(s)}s`;
if (s < 3600) return `${Math.floor(s / 60)}m ${Math.round(s % 60)}s`;
if (s < 86400) return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`;
return `${Math.floor(s / 86400)}d ${Math.floor((s % 86400) / 3600)}h`;
}
function fmtMs(ms) {
if (!ms) return "—";
return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(2)}s`;
}
function fmtTime(ts) {
return new Date(ts * 1000).toLocaleTimeString("en-GB", { hour12: false });
}
function pulse(id) {
const el = $(id); if (!el) return;
el.classList.add("pulse");
setTimeout(() => el.classList.remove("pulse"), 600);
}
let lastTotal = -1;
async function refreshStats() {
try {
const r = await fetch(BASE + "/stats", { cache: "no-store" });
if (!r.ok) return;
const s = await r.json();
$("h_mode").textContent = s.mode + (s.proxy_url ? " → " + s.proxy_url : "");
$("st_uptime").textContent = fmtUptime(s.uptime);
$("st_inflight").textContent = s.in_flight;
$("st_solved").textContent = s.solved;
$("st_challenges").textContent = s.challenges;
$("st_errors").textContent = s.errors;
$("st_success").textContent = s.total_requests ? `${s.success_rate}%` : "—";
$("st_avg").textContent = fmtMs(s.latency_ms.avg);
$("st_p95").textContent = fmtMs(s.latency_ms.p95);
if (lastTotal >= 0 && s.total_requests > lastTotal) {
["st_solved", "st_challenges", "st_errors", "st_success", "st_avg", "st_p95"].forEach(pulse);
}
lastTotal = s.total_requests;
renderEvents(s.events || []);
} catch {}
}
function renderEvents(events) {
$("ev_count").textContent = events.length ? `${events.length} event${events.length === 1 ? "" : "s"}` : "no events";
const el = $("events");
if (!events.length) { el.innerHTML = '<div class="empty">No requests yet.</div>'; return; }
el.innerHTML = events.map((e) => {
const ok = e.status >= 200 && e.status < 400;
return `<div class="event">
<span class="ts">${fmtTime(e.ts)}</span>
<span>${esc(e.endpoint)}</span>
<span class="st ${ok ? "ok" : "err"}">${e.status} · ${fmtMs(e.duration * 1000)}</span>
<span class="sum" title="${esc(e.summary)}">${esc(e.summary)}</span>
</div>`;
}).join("");
}
function esc(s) {
return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
}
updateHint(); renderSnippet(); refreshStats();
setInterval(refreshStats, 3000);
</script>
</body>
</html>