Files
nfc-tool/static/index.html
T

710 lines
22 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ACR122U NFC Studio Pro</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=Fira+Code:wght@400;600&family=Inter:wght@300;400;600;700;800&display=swap" rel="stylesheet">
<style>
:root {
--bg-dark: #0a0c10;
--bg-card: rgba(18, 22, 31, 0.75);
--bg-card-hover: rgba(28, 35, 49, 0.85);
--border-color: rgba(255, 255, 255, 0.08);
--accent-cyan: #00f2fe;
--accent-blue: #4facfe;
--accent-gold: #f6d365;
--accent-emerald: #00b894;
--accent-pink: #ff0844;
--text-main: #f0f4f8;
--text-muted: #8a99ad;
--font-main: 'Inter', system-ui, -apple-system, sans-serif;
--font-mono: 'Fira Code', monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-dark);
background-image:
radial-gradient(at 0% 0%, rgba(0, 242, 254, 0.08) 0px, transparent 50%),
radial-gradient(at 100% 100%, rgba(79, 172, 254, 0.08) 0px, transparent 50%);
color: var(--text-main);
font-family: var(--font-main);
min-height: 100vh;
display: flex;
flex-direction: column;
}
header {
background: rgba(10, 12, 16, 0.8);
backdrop-filter: blur(16px);
border-bottom: 1px solid var(--border-color);
padding: 16px 32px;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 100;
}
.logo-container {
display: flex;
align-items: center;
gap: 12px;
}
.logo-icon {
width: 36px;
height: 36px;
background: linear-gradient(135deg, var(--accent-cyan), var(--accent-blue));
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
color: #000;
box-shadow: 0 0 15px rgba(0, 242, 254, 0.4);
}
.logo-title {
font-size: 20px;
font-weight: 700;
letter-spacing: -0.5px;
background: linear-gradient(135deg, #fff, var(--text-muted));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.status-badge {
display: flex;
align-items: center;
gap: 10px;
background: var(--bg-card);
border: 1px solid var(--border-color);
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #ff4757;
box-shadow: 0 0 8px #ff4757;
transition: all 0.3s ease;
}
.status-dot.active {
background-color: var(--accent-emerald);
box-shadow: 0 0 10px var(--accent-emerald);
}
nav {
display: flex;
gap: 8px;
padding: 16px 32px 0 32px;
border-bottom: 1px solid var(--border-color);
overflow-x: auto;
}
.nav-btn {
background: transparent;
border: none;
color: var(--text-muted);
padding: 12px 20px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
border-radius: 10px 10px 0 0;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
border-bottom: 2px solid transparent;
}
.nav-btn:hover {
color: var(--text-main);
background: rgba(255, 255, 255, 0.03);
}
.nav-btn.active {
color: var(--accent-cyan);
border-bottom: 2px solid var(--accent-cyan);
background: rgba(0, 242, 254, 0.05);
}
main {
flex: 1;
padding: 32px;
max-width: 1400px;
width: 100%;
margin: 0 auto;
}
.tab-content {
display: none;
animation: fadeIn 0.3s ease;
}
.tab-content.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
/* Grid Layouts */
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
@media (max-width: 900px) {
.grid-2 { grid-template-columns: 1fr; }
}
/* Cards */
.glass-card {
background: var(--bg-card);
backdrop-filter: blur(12px);
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 24px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.37);
margin-bottom: 24px;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.card-title {
font-size: 16px;
font-weight: 700;
display: flex;
align-items: center;
gap: 10px;
color: var(--text-main);
}
/* NFC Visual Card */
.nfc-hologram-card {
background: linear-gradient(135deg, #1e293b, #0f172a);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 20px;
padding: 28px;
min-height: 220px;
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
overflow: hidden;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
}
.nfc-hologram-card::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(0, 242, 254, 0.1) 0%, transparent 60%);
pointer-events: none;
}
.chip-icon {
width: 44px;
height: 34px;
background: linear-gradient(135deg, #f6d365, #fda085);
border-radius: 6px;
border: 1px solid rgba(0,0,0,0.3);
}
.card-uid-display {
font-family: var(--font-mono);
font-size: 26px;
font-weight: 700;
color: var(--accent-cyan);
letter-spacing: 2px;
text-shadow: 0 0 10px rgba(0, 242, 254, 0.5);
}
.card-meta {
display: flex;
justify-content: space-between;
font-size: 13px;
color: var(--text-muted);
}
/* Form Controls */
.form-group {
margin-bottom: 20px;
}
label {
display: block;
font-size: 13px;
font-weight: 600;
margin-bottom: 8px;
color: var(--text-muted);
}
input, select, textarea {
width: 100%;
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 12px 16px;
color: var(--text-main);
font-family: inherit;
font-size: 14px;
outline: none;
transition: all 0.2s ease;
}
input:focus, select:focus, textarea:focus {
border-color: var(--accent-cyan);
box-shadow: 0 0 10px rgba(0, 242, 254, 0.2);
}
.btn {
background: linear-gradient(135deg, var(--accent-cyan), var(--accent-blue));
color: #000;
font-weight: 700;
border: none;
padding: 12px 24px;
border-radius: 10px;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 15px rgba(0, 242, 254, 0.3);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 242, 254, 0.5);
}
.btn-secondary {
background: rgba(255, 255, 255, 0.08);
color: var(--text-main);
box-shadow: none;
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.15);
}
/* MIFARE Grid */
.sector-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
}
.sector-card {
background: rgba(0, 0, 0, 0.25);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 14px;
font-family: var(--font-mono);
font-size: 12px;
}
.sector-header {
display: flex;
justify-content: space-between;
font-weight: 700;
color: var(--accent-gold);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
padding-bottom: 6px;
margin-bottom: 8px;
}
.block-line {
display: flex;
justify-content: space-between;
padding: 4px 0;
color: #a0aec0;
}
.block-line.trailer {
color: #d6bcfa;
font-weight: 600;
}
/* APDU Console */
.apdu-console {
background: #05070a;
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 16px;
font-family: var(--font-mono);
min-height: 180px;
max-height: 350px;
overflow-y: auto;
font-size: 13px;
}
.apdu-log {
margin-bottom: 8px;
}
.apdu-in { color: var(--accent-cyan); }
.apdu-out { color: var(--accent-emerald); }
.apdu-err { color: var(--accent-pink); }
</style>
</head>
<body>
<header>
<div class="logo-container">
<div class="logo-icon">NFC</div>
<div>
<div class="logo-title">ACR122U Studio Pro</div>
<div style="font-size: 11px; color: var(--text-muted);">PC/SC NFC Reader & Writer Software</div>
</div>
</div>
<div class="status-badge">
<div id="statusDot" class="status-dot"></div>
<span id="statusText">Verbindung wird aufgebaut...</span>
</div>
</header>
<nav>
<button class="nav-btn active" onclick="switchTab('scanTab', this)">📡 Live Scan</button>
<button class="nav-btn" onclick="switchTab('hardwareTab', this)">💡 Reader Control (LED/Beep)</button>
<button class="nav-btn" onclick="switchTab('mifareTab', this)">🔑 MIFARE Inspector</button>
<button class="nav-btn" onclick="switchTab('ndefTab', this)">🏷️ NTAG & NDEF Studio</button>
<button class="nav-btn" onclick="switchTab('apduTab', this)">⚡ APDU Lab</button>
</nav>
<main>
<!-- TAB 1: LIVE SCAN -->
<div id="scanTab" class="tab-content active">
<div class="grid-2">
<div class="glass-card">
<div class="card-header">
<div class="card-title">💳 Erkannte NFC Karte</div>
</div>
<div class="nfc-hologram-card">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div class="chip-icon"></div>
<span style="font-size: 12px; color: var(--accent-cyan); font-weight: 700;">NFC ISO/IEC 14443</span>
</div>
<div>
<div style="font-size: 11px; color: var(--text-muted); margin-bottom: 4px;">CARD UID</div>
<div id="cardUid" class="card-uid-display">-- -- -- --</div>
</div>
<div class="card-meta">
<div>Typ: <span id="cardType" style="color: #fff;">Keine Karte aufgelegt</span></div>
<div>ATS: <span id="cardAts" style="color: #fff;">--</span></div>
</div>
</div>
</div>
<div class="glass-card">
<div class="card-header">
<div class="card-title">📜 Live Event Stream</div>
</div>
<div id="eventLog" class="apdu-console">
<div class="apdu-log" style="color: var(--text-muted);">Warte auf NFC-Tag Interaktionen...</div>
</div>
</div>
</div>
</div>
<!-- TAB 2: HARDWARE CONTROL -->
<div id="hardwareTab" class="tab-content">
<div class="glass-card">
<div class="card-header">
<div class="card-title">💡 ACR122U LED & Buzzer Steuerung</div>
</div>
<div class="grid-2">
<div>
<div class="form-group">
<label>Rote LED State</label>
<input type="checkbox" id="ledRed">
</div>
<div class="form-group">
<label>Grüne LED State</label>
<input type="checkbox" id="ledGreen" checked>
</div>
<div class="form-group">
<label>Blink-Modus</label>
<input type="checkbox" id="ledBlink">
</div>
<div class="form-group">
<label>Summer (Beep beim Ändern)</label>
<input type="checkbox" id="ledBeep" checked>
</div>
<button class="btn" onclick="applyLedConfig()">LED & Buzzer Anwenden</button>
</div>
<div>
<label>Schnell-Tests für Summer (Beep)</label>
<div style="display: flex; gap: 12px; margin-top: 12px; flex-wrap: wrap;">
<button class="btn btn-secondary" onclick="quickBeep(100, 1)">1x Kurzer Beep</button>
<button class="btn btn-secondary" onclick="quickBeep(100, 2)">2x Doppel-Beep</button>
<button class="btn btn-secondary" onclick="quickBeep(200, 3)">3x Warnung</button>
</div>
</div>
</div>
</div>
</div>
<!-- TAB 3: MIFARE INSPECTOR -->
<div id="mifareTab" class="tab-content">
<div class="glass-card">
<div class="card-header">
<div class="card-title">🔑 MIFARE Classic 1K Memory Map & Key Cracker</div>
<button class="btn" onclick="dumpMifare()">Karte Auslesen / Dump</button>
</div>
<div id="mifareDumpContainer" class="sector-grid">
<div style="color: var(--text-muted); grid-column: 1 / -1;">Klicke auf "Karte Auslesen", um alle 16 Sektoren zu scannen.</div>
</div>
</div>
</div>
<!-- TAB 4: NTAG & NDEF -->
<div id="ndefTab" class="tab-content">
<div class="glass-card">
<div class="card-header">
<div class="card-title">🏷️ NDEF Nachricht auf Tag Schreiben</div>
</div>
<div class="form-group">
<label>NDEF Typ</label>
<select id="ndefType" onchange="toggleNdefFields()">
<option value="url">Webseite / URL (https://...)</option>
<option value="text">Text Nachricht</option>
</select>
</div>
<div id="urlGroup" class="form-group">
<label>Ziel-URL</label>
<input type="text" id="ndefUrl" placeholder="https://example.com">
</div>
<div id="textGroup" class="form-group" style="display: none;">
<label>Text Inhalt</label>
<textarea id="ndefText" rows="3" placeholder="Dein Text hier..."></textarea>
</div>
<button class="btn" onclick="writeNdef()">NFC Tag Beschreiben</button>
</div>
</div>
<!-- TAB 5: APDU LAB -->
<div id="apduTab" class="tab-content">
<div class="glass-card">
<div class="card-header">
<div class="card-title">⚡ Raw APDU Command Studio</div>
</div>
<div class="form-group">
<label>Schnell-Vorlagen</label>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<button class="btn btn-secondary" onclick="setApdu('FF 00 48 00 00')">Firmware Version</button>
<button class="btn btn-secondary" onclick="setApdu('FF CA 00 00 00')">Read UID</button>
<button class="btn btn-secondary" onclick="setApdu('FF CA 01 00 00')">Read ATS</button>
<button class="btn btn-secondary" onclick="setApdu('FF 00 40 0F 04 01 01 01 01')">Test Beep & LED</button>
</div>
</div>
<div class="form-group">
<label>APDU Hex Befehl</label>
<input type="text" id="apduInput" value="FF 00 48 00 00" style="font-family: var(--font-mono);">
</div>
<button class="btn" onclick="sendApdu()">APDU Senden</button>
<div style="margin-top: 24px;">
<label>Antwort vom Leser / Karte</label>
<div id="apduResponseLog" class="apdu-console">
<div class="apdu-log" style="color: var(--text-muted);">Ergebnisse werden hier angezeigt...</div>
</div>
</div>
</div>
</div>
</main>
<script>
let ws;
function connectWebSocket() {
const loc = window.location;
const wsUrl = `ws://${loc.host}/ws`;
ws = new WebSocket(wsUrl);
ws.onopen = () => {
document.getElementById('statusDot').classList.add('active');
document.getElementById('statusText').innerText = 'ACR122U Verbunden';
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
handleWsEvent(data);
};
ws.onclose = () => {
document.getElementById('statusDot').classList.remove('active');
document.getElementById('statusText').innerText = 'Getrennt - Reconnect...';
setTimeout(connectWebSocket, 2000);
};
}
function handleWsEvent(evt) {
const log = document.getElementById('eventLog');
const time = new Date().toLocaleTimeString();
if (evt.event === 'card_detected') {
document.getElementById('cardUid').innerText = evt.uid;
document.getElementById('cardType').innerText = evt.card_type;
document.getElementById('cardAts').innerText = evt.ats || 'None';
logEvent(`<span class="apdu-out">[${time}] 💳 Karte erkannt: UID ${evt.uid} (${evt.card_type})</span>`);
} else if (evt.event === 'card_removed') {
document.getElementById('cardUid').innerText = '-- -- -- --';
document.getElementById('cardType').innerText = 'Keine Karte aufgelegt';
document.getElementById('cardAts').innerText = '--';
logEvent(`<span class="apdu-err">[${time}] 🚫 Karte entfernt</span>`);
}
}
function logEvent(htmlStr) {
const log = document.getElementById('eventLog');
log.innerHTML = htmlStr + log.innerHTML;
}
function switchTab(tabId, btn) {
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
document.getElementById(tabId).classList.add('active');
btn.classList.add('active');
}
function toggleNdefFields() {
const type = document.getElementById('ndefType').value;
document.getElementById('urlGroup').style.display = type === 'url' ? 'block' : 'none';
document.getElementById('textGroup').style.display = type === 'text' ? 'block' : 'none';
}
async function applyLedConfig() {
const payload = {
red: document.getElementById('ledRed').checked,
green: document.getElementById('ledGreen').checked,
blink: document.getElementById('ledBlink').checked,
beep: document.getElementById('ledBeep').checked,
};
await fetch('/api/led', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
}
async function quickBeep(duration, count) {
await fetch('/api/apdu', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ apdu_hex: `FF 00 40 00 04 ${duration/100} ${duration/100} ${count} 01` })
});
}
async function dumpMifare() {
const container = document.getElementById('mifareDumpContainer');
container.innerHTML = '<div style="color: var(--accent-cyan); grid-column: 1 / -1;">Scanne Karte... Bitte Karte ruhig auf dem Leser liegen lassen...</div>';
const res = await fetch('/api/mifare/dump', { method: 'POST' });
const data = await res.json();
if (data.success) {
container.innerHTML = '';
data.dump.sectors.forEach(sec => {
let html = `<div class="sector-card">
<div class="sector-header">
<span>Sektor ${sec.sector_number}</span>
<span>Key A: ${sec.key_a || '❌'}</span>
</div>`;
sec.blocks.forEach(b => {
html += `<div class="block-line ${b.is_trailer ? 'trailer' : ''}">
<span>B${b.block_number}: ${b.hex_data}</span>
<span>${b.ascii_data}</span>
</div>`;
});
html += `</div>`;
container.innerHTML += html;
});
} else {
container.innerHTML = `<div style="color: var(--accent-pink); grid-column: 1 / -1;">Fehler beim Auslesen: ${data.error}</div>`;
}
}
async function writeNdef() {
const type = document.getElementById('ndefType').value;
let url = '/api/ndef/write-url';
let payload = {};
if (type === 'url') {
payload = { url: document.getElementById('ndefUrl').value };
} else {
url = '/api/ndef/write-text';
payload = { text: document.getElementById('ndefText').value, lang: 'de' };
}
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.success) {
alert('✅ NDEF erfolgreich auf NFC Tag geschrieben!');
} else {
alert('❌ Fehler: ' + data.error);
}
}
function setApdu(hexStr) {
document.getElementById('apduInput').value = hexStr;
}
async function sendApdu() {
const hexStr = document.getElementById('apduInput').value;
const res = await fetch('/api/apdu', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ apdu_hex: hexStr })
});
const data = await res.json();
const log = document.getElementById('apduResponseLog');
if (data.success) {
log.innerHTML = `<div class="apdu-log apdu-in">> ${hexStr}</div>` +
`<div class="apdu-log apdu-out">< DATEN: ${data.response.data_hex || '(Keine Daten)'}</div>` +
`<div class="apdu-log apdu-out">< SW1/SW2: ${data.response.status}</div>` +
log.innerHTML;
} else {
log.innerHTML = `<div class="apdu-log apdu-err">❌ FEHLER: ${data.error}</div>` + log.innerHTML;
}
}
connectWebSocket();
</script>
</body>
</html>