710 lines
22 KiB
HTML
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>
|