Add standalone /tools page for bookmarkable quantum random tools

Dedicated tools-only page at /tools with dice, password generator,
coin flip, and 8 ball - each with its own click-to-copy output box.
Clean, lightweight page without API docs/MCP/contact form clutter.
Linked from the main page's QTRNG Tools section.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leopere 2026-02-08 16:45:14 -05:00
parent 7bf292d64e
commit b7bff60271
Signed by: colin
SSH Key Fingerprint: SHA256:nRPCQTeMFLdGytxRQmPVK9VXY3/ePKQ5lGRyJhT5DY8
3 changed files with 330 additions and 1 deletions

View File

@ -397,7 +397,7 @@
<div class="card">
<h2>QTRNG Tools</h2>
<p style="margin-bottom:1.25rem;color:var(--date-color);">Quantum dice, passwords, coin flips, and a Magic 8 Ball — all powered by quantum camera noise.</p>
<p style="margin-bottom:1.25rem;color:var(--date-color);">Quantum dice, passwords, coin flips, and a Magic 8 Ball — all powered by quantum camera noise. <a href="/tools" title="Bookmark this page">Standalone tools page &rarr;</a></p>
<div class="tools-section">
<h3>Quantum Dice</h3>

View File

@ -75,6 +75,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.route("/password", get(get_password))
.route("/coin", get(get_coin))
.route("/8ball", get(get_eightball))
.route("/tools", get(tools_page))
.route("/health", get(health))
.route("/.well-known/mcp.json", get(mcp_wellknown))
.route("/.well-known/skill.md", get(get_skill_md))
@ -90,6 +91,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
async fn index() -> Html<&'static str> { Html(INDEX_HTML) }
async fn tools_page() -> Html<&'static str> { Html(TOOLS_HTML) }
async fn health() -> &'static str { "ok" }
async fn get_docs() -> Html<String> {
@ -423,4 +425,5 @@ async fn get_stream(Query(params): Query<StreamQuery>) -> Response {
const INDEX_HTML: &str = include_str!("index.html");
const TOOLS_HTML: &str = include_str!("tools.html");
const SKILL_MD: &str = include_str!("../skill.md");

326
src/tools.html Normal file
View File

@ -0,0 +1,326 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Quantum Random Tools - Dice, passwords, coin flips, and Magic 8 Ball powered by camera sensor quantum noise.">
<title>Quantum Random Tools - Camera TRNG</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color: #ffffff; --text-color: #333333; --accent-color: #0056b3;
--border-color: #e0e0e0; --hover-color: #003d82; --theme-bg: #f5f5f5;
--theme-hover: #e0e0e0; --date-color: #555555;
--bg-primary: #ffffff; --bg-secondary: #f5f5f5; --bg-tertiary: #eaeaea;
--text-primary: #333333; --focus-outline-color: #0056b3;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #1a1a1a; --text-color: #e0e0e0; --accent-color: #5fa9ff;
--border-color: #404040; --hover-color: #8ac2ff; --theme-bg: #2d2d2d;
--theme-hover: #3d3d3d; --date-color: #a0a0a0;
--bg-primary: #1a1a1a; --bg-secondary: #2d2d2d; --bg-tertiary: #3d3d3d;
--text-primary: #e0e0e0; --focus-outline-color: #5fa9ff;
}
}
html[data-theme='light'] {
--bg-color: #ffffff; --text-color: #333333; --accent-color: #0056b3;
--border-color: #e0e0e0; --hover-color: #003d82; --theme-bg: #f5f5f5;
--theme-hover: #e0e0e0; --date-color: #555555;
--bg-primary: #ffffff; --bg-secondary: #f5f5f5; --bg-tertiary: #eaeaea;
--text-primary: #333333; --focus-outline-color: #0056b3;
}
html[data-theme='dark'] {
--bg-color: #1a1a1a; --text-color: #e0e0e0; --accent-color: #5fa9ff;
--border-color: #404040; --hover-color: #8ac2ff; --theme-bg: #2d2d2d;
--theme-hover: #3d3d3d; --date-color: #a0a0a0;
--bg-primary: #1a1a1a; --bg-secondary: #2d2d2d; --bg-tertiary: #3d3d3d;
--text-primary: #e0e0e0; --focus-outline-color: #5fa9ff;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
line-height: 1.6; color: var(--text-color); background-color: var(--bg-color);
margin: 0 auto; padding: 20px; max-width: 700px;
}
a { color: var(--accent-color); text-decoration: underline; }
a:hover { color: var(--hover-color); }
.theme-switch { position: fixed; top: 20px; right: 20px; z-index: 1000; }
.theme-switch button {
background: var(--theme-bg); border: 1px solid var(--border-color); font-size: 1.5em;
cursor: pointer; padding: 8px 12px; border-radius: 8px; transition: background-color 0.3s;
}
.theme-switch button:hover { background-color: var(--theme-hover); }
h1 {
font-size: 1.75em; color: var(--accent-color); border-bottom: 2px solid var(--accent-color);
padding-bottom: 0.3em; margin-bottom: 0.25em;
}
h3 { font-size: 1.1em; color: var(--text-color); margin-top: 0; margin-bottom: 0.75rem; }
.subtitle { color: var(--date-color); font-size: 0.9rem; margin-bottom: 1.5rem; }
.back-link { font-size: 0.85rem; margin-bottom: 1rem; display: block; }
.card {
background: var(--bg-secondary); border: 1px solid var(--border-color);
border-radius: 8px; padding: 1.5rem; margin-bottom: 1.25rem;
}
.controls { display: flex; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap; align-items: flex-end; }
.field { flex: 1; min-width: 120px; }
label {
display: block; font-size: 0.85rem; color: var(--date-color);
margin-bottom: 0.5rem; text-transform: uppercase; letter-spacing: 0.05em;
}
input, select {
width: 100%; padding: 0.75rem 1rem; background: var(--bg-primary);
border: 1px solid var(--border-color); border-radius: 4px;
color: var(--text-color); font-family: monospace; font-size: 0.9rem;
}
input:focus, select:focus {
outline: none; border-color: var(--accent-color); box-shadow: 0 0 0 3px rgba(0,86,179,0.1);
}
.btn-primary {
padding: 0.75rem 2rem; background: var(--accent-color); border: none; border-radius: 4px;
color: white; font-weight: 600; font-size: 1rem; cursor: pointer; transition: background 0.2s;
}
.btn-primary:hover { background: var(--hover-color); }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
.output {
background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 4px;
padding: 1rem; font-family: monospace; font-size: 0.85rem; word-break: break-all;
line-height: 1.6; min-height: 60px; max-height: 120px; overflow-y: auto; color: var(--accent-color);
}
.output.error { color: #dc3545; }
.output.copyable { cursor: pointer; position: relative; }
.output.copyable:hover { border-color: var(--accent-color); }
.copy-toast {
position: fixed; bottom: 2rem; left: 50%; transform: translateX(-50%);
background: var(--accent-color); color: #fff; padding: 0.5rem 1.25rem;
border-radius: 6px; font-size: 0.85rem; font-weight: 500;
opacity: 0; transition: opacity 0.25s; pointer-events: none; z-index: 9999;
}
.copy-toast.show { opacity: 1; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.loading { animation: pulse 1s infinite; }
.eightball-answer {
font-size: 1.3rem; font-weight: 600; text-align: center; padding: 1.5rem;
border-radius: 50%; width: 180px; height: 180px; margin: 1rem auto;
display: flex; align-items: center; justify-content: center;
background: radial-gradient(circle at 40% 40%, #333, #111);
color: #fff; font-family: serif; line-height: 1.3;
box-shadow: 0 4px 20px rgba(0,0,0,0.3); transition: transform 0.3s;
}
.eightball-answer:hover { transform: scale(1.05); }
.eightball-answer.positive { color: #4cff72; }
.eightball-answer.neutral { color: #ffd644; }
.eightball-answer.negative { color: #ff6b6b; }
@keyframes shake8ball {
0%, 100% { transform: rotate(0deg); }
10% { transform: rotate(-8deg); } 20% { transform: rotate(8deg); }
30% { transform: rotate(-6deg); } 40% { transform: rotate(6deg); }
50% { transform: rotate(-3deg); } 60% { transform: rotate(3deg); }
70% { transform: rotate(-1deg); } 80% { transform: rotate(1deg); }
}
.eightball-shaking { animation: shake8ball 0.6s ease-in-out; }
@media (max-width: 600px) {
body { padding: 10px; } h1 { font-size: 1.5em; }
.controls { flex-direction: column; }
}
</style>
</head>
<body>
<div class="theme-switch">
<button id="themeToggle" type="button" role="switch" aria-label="Theme mode: Auto">&#x1F313;</button>
</div>
<a href="/" class="back-link">&larr; Back to Camera TRNG</a>
<h1>Quantum Random Tools</h1>
<p class="subtitle">Dice, passwords, coin flips, and a Magic 8 Ball &mdash; all from quantum camera noise.</p>
<!-- Dice -->
<div class="card">
<h3>Quantum Dice</h3>
<div class="controls">
<div class="field">
<label for="dice-sides">Sides (d)</label>
<input type="number" id="dice-sides" value="6" min="2" max="256">
</div>
<div class="field">
<label for="dice-count">Count</label>
<input type="number" id="dice-count" value="1" min="1" max="100">
</div>
<button id="btn-dice" class="btn-primary">Roll</button>
</div>
<div class="output copyable" id="out-dice" title="Click to copy">Roll some dice...</div>
</div>
<!-- Password -->
<div class="card">
<h3>Quantum Password</h3>
<div class="controls">
<div class="field">
<label for="pw-length">Length</label>
<input type="number" id="pw-length" value="16" min="1" max="128">
</div>
<div class="field">
<label for="pw-charset">Charset</label>
<select id="pw-charset">
<option value="alphanumeric">Alphanumeric</option>
<option value="full">Full (+ symbols)</option>
<option value="hex">Hex</option>
</select>
</div>
<div class="field" style="display:flex;align-items:flex-end;gap:0.5rem;">
<label for="pw-no-ambiguous" style="text-transform:none;white-space:nowrap;cursor:pointer;display:flex;align-items:center;gap:0.4rem;">
<input type="checkbox" id="pw-no-ambiguous" checked>
Exclude ambiguous <code style="font-size:0.8em;opacity:0.7;">0 O o I l 1 |</code>
</label>
</div>
<button id="btn-password" class="btn-primary">Generate</button>
</div>
<div class="output copyable" id="out-password" title="Click to copy">Generate a password...</div>
</div>
<!-- Coin -->
<div class="card">
<h3>Quantum Coin Flip</h3>
<div class="controls">
<button id="btn-coin" class="btn-primary">Flip</button>
</div>
<div class="output copyable" id="out-coin" title="Click to copy">Flip a coin...</div>
</div>
<!-- 8 Ball -->
<div class="card">
<h3>Quantum 8 Ball</h3>
<div class="controls">
<div class="field" style="flex:3;">
<label for="eightball-question">Ask the Quantum Void</label>
<input type="text" id="eightball-question" placeholder="Will I ever understand quantum mechanics?" maxlength="200">
</div>
<button id="btn-eightball" class="btn-primary">Shake</button>
</div>
<div class="eightball-answer" id="eightball-orb" style="display:none;"></div>
<div class="output copyable" id="out-eightball" title="Click to copy">Shake the 8 ball...</div>
</div>
<div class="copy-toast" id="copy-toast">Copied!</div>
<script>
// Theme toggle (shared with main page via localStorage)
const themeToggle = document.getElementById('themeToggle');
const html = document.documentElement;
const saved = localStorage.getItem('theme') || 'auto';
function setTheme(t) {
const icons = { light: '\u{1F31E}', dark: '\u{1F319}', auto: '\u{1F313}' };
themeToggle.textContent = icons[t];
themeToggle.setAttribute('aria-label', 'Theme: ' + t);
if (t === 'auto') html.removeAttribute('data-theme');
else html.setAttribute('data-theme', t);
}
setTheme(saved);
themeToggle.addEventListener('click', () => {
const cur = html.getAttribute('data-theme') || 'auto';
const next = cur === 'light' ? 'dark' : cur === 'dark' ? 'auto' : 'light';
setTheme(next); localStorage.setItem('theme', next);
});
// Click-to-copy
const copyToast = document.getElementById('copy-toast');
let copyTimer;
function copyText(el) {
const t = el.textContent;
if (!t || t.endsWith('...') || t.startsWith('Error:')) return;
navigator.clipboard.writeText(t).then(() => {
clearTimeout(copyTimer);
copyToast.classList.add('show');
copyTimer = setTimeout(() => copyToast.classList.remove('show'), 1500);
});
}
const origin = window.location.origin;
const outDice = document.getElementById('out-dice');
const outPw = document.getElementById('out-password');
const outCoin = document.getElementById('out-coin');
const outBall = document.getElementById('out-eightball');
const orb = document.getElementById('eightball-orb');
// Wire click-to-copy on every output box
[outDice, outPw, outCoin, outBall].forEach(el => el.addEventListener('click', () => copyText(el)));
const allBtns = ['btn-dice','btn-password','btn-coin','btn-eightball'].map(id => document.getElementById(id));
function disableAll(v) { allBtns.forEach(b => b.disabled = v); }
// Dice
document.getElementById('btn-dice').addEventListener('click', async () => {
disableAll(true); outDice.className = 'output copyable loading'; outDice.textContent = 'Rolling...';
try {
const d = document.getElementById('dice-sides').value || 6;
const c = document.getElementById('dice-count').value || 1;
const r = await fetch(origin + '/dice?d=' + d + '&count=' + c);
if (!r.ok) throw new Error(await r.text());
const j = await r.json();
outDice.className = 'output copyable';
outDice.textContent = 'Rolls: ' + j.rolls.join(', ') + (j.count > 1 ? ' Sum: ' + j.sum : '');
} catch (e) { outDice.className = 'output copyable error'; outDice.textContent = 'Error: ' + e.message; }
disableAll(false);
});
// Password
document.getElementById('btn-password').addEventListener('click', async () => {
disableAll(true); outPw.className = 'output copyable loading'; outPw.textContent = 'Generating...';
try {
const len = document.getElementById('pw-length').value || 16;
const cs = document.getElementById('pw-charset').value || 'alphanumeric';
const noAmb = document.getElementById('pw-no-ambiguous').checked;
let url = origin + '/password?length=' + len + '&charset=' + cs;
if (noAmb) url += '&exclude_ambiguous=true';
const r = await fetch(url);
if (!r.ok) throw new Error(await r.text());
const j = await r.json();
outPw.className = 'output copyable';
outPw.style.fontFamily = "'SF Mono', Monaco, 'Cascadia Code', 'Fira Code', monospace";
outPw.style.fontSize = '1.1rem';
outPw.style.letterSpacing = '0.08em';
outPw.textContent = j.password;
} catch (e) { outPw.className = 'output copyable error'; outPw.textContent = 'Error: ' + e.message; }
disableAll(false);
});
// Coin
document.getElementById('btn-coin').addEventListener('click', async () => {
disableAll(true); outCoin.className = 'output copyable loading'; outCoin.textContent = 'Flipping...';
try {
const r = await fetch(origin + '/coin');
if (!r.ok) throw new Error(await r.text());
const j = await r.json();
outCoin.className = 'output copyable';
outCoin.textContent = j.result.charAt(0).toUpperCase() + j.result.slice(1);
} catch (e) { outCoin.className = 'output copyable error'; outCoin.textContent = 'Error: ' + e.message; }
disableAll(false);
});
// 8 Ball
document.getElementById('btn-eightball').addEventListener('click', async () => {
disableAll(true); outBall.className = 'output copyable loading'; outBall.textContent = '';
orb.style.display = 'flex';
orb.className = 'eightball-answer eightball-shaking'; orb.textContent = '...';
try {
const q = document.getElementById('eightball-question').value;
let url = origin + '/8ball';
if (q) url += '?question=' + encodeURIComponent(q);
const r = await fetch(url);
if (!r.ok) throw new Error(await r.text());
const j = await r.json();
orb.className = 'eightball-answer ' + j.sentiment;
orb.textContent = j.answer;
outBall.className = 'output copyable';
outBall.textContent = (q ? 'Q: ' + q + ' \u2192 ' : '') + j.answer;
} catch (e) {
orb.style.display = 'none';
outBall.className = 'output copyable error'; outBall.textContent = 'Error: ' + e.message;
}
disableAll(false);
});
</script>
</body>
</html>