pysim tab: file browser, AID display, card detection, server simplification
This commit is contained in:
+175
-140
@@ -824,47 +824,27 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="pysim-connect-info" class="mb-3 text-xs text-gray-500 dark:text-slate-400 leading-relaxed">
|
||||
<p>To connect, install and start the <b>pysim-otaman-server</b>:</p>
|
||||
<p><code class="font-mono bg-gray-100 dark:bg-slate-700 px-1 rounded">pip install pysim-otaman-server</code></p>
|
||||
<p><code class="font-mono bg-gray-100 dark:bg-slate-700 px-1 rounded">pysim-otaman-server --http-port 8080</code></p>
|
||||
<p class="mt-1">Then connect to a SIM card reader and click <b>Connect</b> above.</p>
|
||||
<p>To connect, install <a href="https://github.com/anttro/pysim-otaman-server" target="_blank" class="text-blue-600 dark:text-blue-400 underline">pysim-otaman-server</a>: <code class="font-mono bg-gray-100 dark:bg-slate-700 px-1 rounded">git clone https://github.com/anttro/pysim-otaman-server.git</code> and start it. Refer to README.md for details. Click <b>Connect</b> once pysim-otaman-server is ready.</p>
|
||||
</div>
|
||||
<div id="pysim-connected-row" class="mb-3 hidden">
|
||||
<div class="flex items-center gap-2">
|
||||
<span id="pysim-status-dot" class="inline-block w-3 h-3 rounded-full bg-green-500"></span>
|
||||
<span class="text-sm font-medium">Connected</span>
|
||||
<button onclick="pysimStatusCmd()" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300">Status</button>
|
||||
<button onclick="pysimResetCmd()" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300">Reset</button>
|
||||
<button onclick="pysimEquipCmd()" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300">Equip</button>
|
||||
<button onclick="pysimDisconnect()" id="pysim-disconnect-btn" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300">Disconnect</button>
|
||||
<button onclick="pysimEquipCmd()" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300">Equip card</button>
|
||||
<button onclick="pysimStatusCmd()" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300">Check status</button>
|
||||
<button onclick="pysimResetCmd()" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300">Reset card</button>
|
||||
<button onclick="pysimDisconnect()" id="pysim-disconnect-btn" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300">Disconnect from pySim</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="pysim-status" class="mb-3 text-xs font-mono text-gray-600 dark:text-slate-400"></div>
|
||||
|
||||
<div class="flex gap-1 mb-2">
|
||||
<button class="pysim-subtab px-3 py-1 text-xs rounded-full bg-blue-600 text-white" data-pysim-sub="cmd">Command</button>
|
||||
<button class="pysim-subtab px-3 py-1 text-xs rounded-full bg-blue-600 text-white" data-pysim-sub="files">File manager</button>
|
||||
<button class="pysim-subtab px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="cmd">pySim command line</button>
|
||||
<button class="pysim-subtab px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="apdu">Raw APDU</button>
|
||||
<button class="pysim-subtab px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="files">Files</button>
|
||||
</div>
|
||||
|
||||
<div id="pysim-sub-cmd">
|
||||
<div class="flex gap-2 items-center mb-2">
|
||||
<input id="pysim-cmd" class="font-mono flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800" placeholder="select MF" autocomplete="off" list="pysim-cmdlist" onkeydown="pysimCmdKeydown(event)">
|
||||
<datalist id="pysim-cmdlist"></datalist>
|
||||
<button onclick="pysimExec()" class="px-4 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700">Execute</button>
|
||||
</div>
|
||||
<textarea id="pysim-cmd-output" rows="10" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 bg-gray-100 dark:bg-slate-800"></textarea>
|
||||
</div>
|
||||
|
||||
<div id="pysim-sub-apdu" class="hidden">
|
||||
<div class="flex gap-2 items-center mb-2">
|
||||
<input id="pysim-apdu" class="font-mono flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800" placeholder="00A4040000" onkeydown="if(event.key==='Enter')pysimApdu()">
|
||||
<button onclick="pysimApdu()" class="px-4 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700">Send</button>
|
||||
</div>
|
||||
<textarea id="pysim-apdu-output" rows="5" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 bg-gray-100 dark:bg-slate-800"></textarea>
|
||||
</div>
|
||||
|
||||
<div id="pysim-sub-files" class="hidden">
|
||||
<div id="pysim-sub-files">
|
||||
<div class="flex gap-2">
|
||||
<div class="w-1/3 border border-gray-300 dark:border-slate-600 rounded p-2 text-xs font-mono overflow-y-auto bg-gray-50 dark:bg-slate-800" style="max-height:420px">
|
||||
<div id="pysim-fs-tree"></div>
|
||||
@@ -884,6 +864,23 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pysim-sub-cmd" class="hidden">
|
||||
<div class="flex gap-2 items-center mb-2">
|
||||
<input id="pysim-cmd" class="font-mono flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800" placeholder="select MF" autocomplete="off" list="pysim-cmdlist" onkeydown="pysimCmdKeydown(event)">
|
||||
<datalist id="pysim-cmdlist"></datalist>
|
||||
<button onclick="pysimExec()" class="px-4 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700">Execute</button>
|
||||
</div>
|
||||
<textarea id="pysim-cmd-output" rows="10" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 bg-gray-100 dark:bg-slate-800"></textarea>
|
||||
</div>
|
||||
|
||||
<div id="pysim-sub-apdu" class="hidden">
|
||||
<div class="flex gap-2 items-center mb-2">
|
||||
<input id="pysim-apdu" class="font-mono flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800" placeholder="00A4040000" onkeydown="if(event.key==='Enter')pysimApdu()">
|
||||
<button onclick="pysimApdu()" class="px-4 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700">Send</button>
|
||||
</div>
|
||||
<textarea id="pysim-apdu-output" rows="5" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 bg-gray-100 dark:bg-slate-800"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -2490,14 +2487,34 @@ async function pysimConnect() {
|
||||
const btn = document.getElementById('pysim-connect-btn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Connecting...';
|
||||
const ok = await pysimRefresh();
|
||||
btn.textContent = ok ? 'Connected' : 'Connect';
|
||||
btn.disabled = false;
|
||||
if (ok) {
|
||||
const statusEl = document.getElementById('pysim-status');
|
||||
statusEl.innerHTML = '<span class="text-gray-500">Checking card...</span>';
|
||||
try {
|
||||
const status = await pysimFetch('/api/status');
|
||||
if (!status.app_ready) {
|
||||
throw new Error('Server not ready');
|
||||
}
|
||||
const cmd = await pysimFetch('/api/command', { cmd: 'status' });
|
||||
if (cmd.output && (cmd.output.includes('Exception') || cmd.output.includes('ProtocolError'))) {
|
||||
pysimSetDot(false);
|
||||
statusEl.innerHTML = '<span class="text-red-500">No card detected. Insert card and click Equip card button</span>';
|
||||
pysimShowConnected(true);
|
||||
btn.textContent = 'Connected';
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
pysimSetDot(true);
|
||||
pysimShowConnected(true);
|
||||
await pysimRefresh();
|
||||
pysimLoadCommands();
|
||||
pysimFsRefresh();
|
||||
btn.textContent = 'Connected';
|
||||
} catch (e) {
|
||||
pysimSetDot(false);
|
||||
statusEl.innerHTML = '<span class="text-red-500">Connection failed: ' + esc(e.message) + '</span>';
|
||||
btn.textContent = 'Connect';
|
||||
}
|
||||
btn.disabled = false;
|
||||
}
|
||||
|
||||
function pysimDisconnect() {
|
||||
@@ -2506,21 +2523,48 @@ function pysimDisconnect() {
|
||||
pysimSetDot(false);
|
||||
}
|
||||
|
||||
async function pysimRunCmd(cmd) {
|
||||
const statusEl = document.getElementById('pysim-status');
|
||||
try {
|
||||
const result = await pysimFetch('/api/command', { cmd });
|
||||
if (result.output && (result.output.includes('Exception') || result.output.includes('ProtocolError') || result.output.includes('SwMatchError'))) {
|
||||
pysimSetDot(false);
|
||||
statusEl.innerHTML = '<span class="text-red-500">No card detected. Insert card and click Equip card button</span>';
|
||||
return null;
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
pysimSetDot(false);
|
||||
statusEl.innerHTML = '<span class="text-red-500">No card detected. Insert card and click Equip card button</span>';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function pysimStatusCmd() {
|
||||
await pysimFetch('/api/command', { cmd: 'status' });
|
||||
pysimRefresh();
|
||||
const statusEl = document.getElementById('pysim-status');
|
||||
statusEl.innerHTML = '<span class="text-gray-500">Checking...</span>';
|
||||
const result = await pysimRunCmd('status');
|
||||
if (result) await pysimRefresh();
|
||||
}
|
||||
|
||||
async function pysimResetCmd() {
|
||||
await pysimFetch('/api/command', { cmd: 'reset' });
|
||||
pysimRefresh();
|
||||
pysimFsRefresh();
|
||||
const statusEl = document.getElementById('pysim-status');
|
||||
statusEl.innerHTML = '<span class="text-gray-500">Resetting...</span>';
|
||||
const result = await pysimRunCmd('reset');
|
||||
if (result) {
|
||||
await pysimRefresh();
|
||||
pysimFsRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
async function pysimEquipCmd() {
|
||||
await pysimFetch('/api/command', { cmd: 'equip' });
|
||||
pysimRefresh();
|
||||
pysimFsRefresh();
|
||||
const statusEl = document.getElementById('pysim-status');
|
||||
statusEl.innerHTML = '<span class="text-gray-500">Equipping...</span>';
|
||||
const result = await pysimRunCmd('equip');
|
||||
if (result) {
|
||||
await pysimRefresh();
|
||||
pysimFsRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
async function pysimExec() {
|
||||
@@ -2614,68 +2658,66 @@ async function pysimLoadCommands() {
|
||||
// ===== pysim file browser =====
|
||||
let pysimFsTreeRoot = null;
|
||||
|
||||
async function pysimFsSelectNode(node) {
|
||||
let sel;
|
||||
if (node.name === 'MF') {
|
||||
sel = 'MF';
|
||||
} else if (node.name.startsWith('ADF.')) {
|
||||
sel = node.name;
|
||||
} else if (node.fid) {
|
||||
sel = node.fid;
|
||||
} else {
|
||||
sel = node.name;
|
||||
}
|
||||
await pysimFetch('/api/command', { cmd: 'select ' + sel });
|
||||
function getParentSel(node) {
|
||||
if (!node || !node.parent) return null;
|
||||
const p = node.parent;
|
||||
if (p.name === 'MF') return 'MF';
|
||||
if (p.name.startsWith('ADF.')) return p.name;
|
||||
if (p.fid) return p.fid;
|
||||
return p.name;
|
||||
}
|
||||
|
||||
async function pysimFsRefresh() {
|
||||
const detailEl = document.getElementById('pysim-fs-detail');
|
||||
detailEl.classList.add('hidden');
|
||||
await pysimFetch('/api/command', { cmd: 'select MF' });
|
||||
const status = await pysimFetch('/api/status');
|
||||
const cur = status.current_selection;
|
||||
const data = await pysimFetch('/api/tree', { name: 'MF' });
|
||||
pysimFsTreeRoot = {
|
||||
name: cur ? cur.name : 'MF',
|
||||
path: cur ? cur.path : 'MF',
|
||||
fid: cur ? cur.fid : '3F00',
|
||||
name: data.name || 'MF',
|
||||
path: data.name || 'MF',
|
||||
fid: data.fid || '3F00',
|
||||
isDir: true,
|
||||
children: null,
|
||||
expanded: true,
|
||||
parent: null,
|
||||
};
|
||||
await pysimFsLoadChildren(pysimFsTreeRoot);
|
||||
pysimFsTreeRoot.children = (data.children || []).map(c => ({
|
||||
name: c.name,
|
||||
fid: c.fid,
|
||||
isDir: c.isDir,
|
||||
aid: c.aid,
|
||||
children: null,
|
||||
expanded: false,
|
||||
exists: true,
|
||||
parent: pysimFsTreeRoot,
|
||||
}));
|
||||
pysimFsTreeRoot.exists = true;
|
||||
pysimFsRenderTree();
|
||||
}
|
||||
|
||||
async function pysimFsLoadChildren(node) {
|
||||
if (node.children) return;
|
||||
try {
|
||||
await pysimFsSelectNode(node);
|
||||
const st = await pysimFetch('/api/status');
|
||||
if (st.current_selection && st.current_selection.name !== node.name) {
|
||||
let body = { name: node.name, fid: node.fid };
|
||||
const parentSel = getParentSel(node);
|
||||
if (parentSel) body.parent_sel = parentSel;
|
||||
let data = await pysimFetch('/api/tree', body);
|
||||
if (!data || data.exists === false) {
|
||||
body = { name: node.name, fid: node.fid };
|
||||
data = await pysimFetch('/api/tree', body);
|
||||
}
|
||||
if (!data || data.exists === false) {
|
||||
throw new Error('File not found');
|
||||
}
|
||||
const data = await pysimFetch('/api/command', { cmd: 'tree' });
|
||||
const lines = (data.output || '').split('\n');
|
||||
const children = [];
|
||||
for (const line of lines) {
|
||||
if (line.startsWith(' ')) continue;
|
||||
const m = line.match(/^(\S+)\s+([0-9a-fA-F]{4})?(?:\s|$)/);
|
||||
if (m) {
|
||||
const name = m[1];
|
||||
const fid = m[2] ? m[2].toLowerCase() : null;
|
||||
children.push({
|
||||
name: name,
|
||||
fid: fid,
|
||||
isDir: name.startsWith('ADF.') || name.startsWith('DF.') || name === 'MF',
|
||||
children: null,
|
||||
expanded: false,
|
||||
exists: true,
|
||||
parent: node,
|
||||
});
|
||||
}
|
||||
}
|
||||
node.children = children;
|
||||
node.children = (data.children || []).map(c => ({
|
||||
name: c.name,
|
||||
fid: c.fid,
|
||||
isDir: c.isDir,
|
||||
aid: c.aid,
|
||||
children: null,
|
||||
expanded: false,
|
||||
exists: true,
|
||||
parent: node,
|
||||
}));
|
||||
node.exists = true;
|
||||
} catch (e) {
|
||||
node.exists = false;
|
||||
@@ -2696,6 +2738,8 @@ function pysimFsRenderNode(node, depth) {
|
||||
const isDir = node.isDir;
|
||||
const exists = node.exists !== false;
|
||||
const color = exists ? (isDir ? 'text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-slate-300') : 'text-red-400 dark:text-red-400';
|
||||
const fidStr = node.fid ? ' <span class="text-gray-400 dark:text-slate-500">(' + node.fid.toUpperCase() + ')</span>' : '';
|
||||
const aidStr = node.aid ? ' <span class="text-gray-400 dark:text-slate-500">[' + node.aid + ']</span>' : '';
|
||||
let html = '<div class="' + color + '">';
|
||||
html += pad;
|
||||
if (!exists) {
|
||||
@@ -2705,14 +2749,14 @@ function pysimFsRenderNode(node, depth) {
|
||||
if (exists) {
|
||||
const toggle = node.children && node.expanded ? '▼' : '▶';
|
||||
html += '<span class="cursor-pointer select-none" onclick="pysimFsToggleDir(\'' + esc(node.name) + '\')">' + toggle + '</span> ';
|
||||
html += '<span class="cursor-pointer hover:underline" onclick="pysimFsToggleDir(\'' + esc(node.name) + '\')">' + esc(node.name) + '</span>';
|
||||
html += '<span class="cursor-pointer hover:underline" onclick="pysimFsToggleDir(\'' + esc(node.name) + '\')">' + esc(node.name) + fidStr + aidStr + '</span>';
|
||||
} else {
|
||||
html += esc(node.name) + '</div>';
|
||||
html += esc(node.name) + fidStr + aidStr + '</div>';
|
||||
return html;
|
||||
}
|
||||
} else {
|
||||
html += '<span class="' + (exists ? 'cursor-pointer hover:underline ml-4' : 'ml-4') + '"' +
|
||||
(exists ? ' onclick="pysimFsClickFile(\'' + esc(node.name) + '\')"' : '') + '>' + esc(node.name) + '</span>';
|
||||
(exists ? ' onclick="pysimFsClickFile(\'' + esc(node.name) + '\')"' : '') + '>' + esc(node.name) + fidStr + aidStr + '</span>';
|
||||
}
|
||||
html += '</div>';
|
||||
if (isDir && node.children && node.expanded) {
|
||||
@@ -2749,77 +2793,68 @@ async function pysimFsToggleDir(name) {
|
||||
async function pysimFsClickFile(name) {
|
||||
pysimFsSetMode('raw');
|
||||
const node = pysimFsFindNode(name, pysimFsTreeRoot);
|
||||
if (node && node.fid) {
|
||||
await pysimFetch('/api/command', { cmd: 'select ' + node.fid });
|
||||
} else {
|
||||
await pysimFetch('/api/command', { cmd: 'select ' + name });
|
||||
let body = { name: name, fid: node ? node.fid : null };
|
||||
const parentSel = getParentSel(node);
|
||||
if (parentSel) body.parent_sel = parentSel;
|
||||
let sel = await pysimFetch('/api/select', body);
|
||||
if (!sel || sel.exists === false) {
|
||||
body = { name: name, fid: node ? node.fid : null };
|
||||
sel = await pysimFetch('/api/select', body);
|
||||
}
|
||||
const status = await pysimFetch('/api/status');
|
||||
const ft = status.current_selection?.file_type;
|
||||
const exists = sel && sel.exists !== false;
|
||||
if (node) {
|
||||
node.exists = exists;
|
||||
if (!exists) node.children = [];
|
||||
pysimFsRenderTree();
|
||||
}
|
||||
const ft = exists ? sel.file_type : null;
|
||||
const label = ({ transparent: 'TR', linear_fixed: 'LF', cyclic: 'CY', ber_tlv: 'BT', df: 'DF' })[ft] || '';
|
||||
document.getElementById('pysim-fs-filename').textContent = name + (label ? ' [' + label + ']' : '');
|
||||
const prefix = exists ? '' : '✗ ';
|
||||
document.getElementById('pysim-fs-filename').textContent = prefix + name + (label ? ' [' + label + ']' : '');
|
||||
document.getElementById('pysim-fs-content').innerHTML = '';
|
||||
document.getElementById('pysim-fs-status').textContent = '';
|
||||
document.getElementById('pysim-fs-detail').classList.remove('hidden');
|
||||
document.getElementById('pysim-fs-status').textContent = exists ? '' : '✗ File not found on card';
|
||||
if (exists) {
|
||||
document.getElementById('pysim-fs-detail').classList.remove('hidden');
|
||||
} else {
|
||||
document.getElementById('pysim-fs-detail').classList.add('hidden');
|
||||
}
|
||||
pysimRefresh();
|
||||
}
|
||||
|
||||
async function pysimFsRead() {
|
||||
const name = document.getElementById('pysim-fs-filename').textContent;
|
||||
const rawName = document.getElementById('pysim-fs-filename').textContent.replace(/ \[.*?\]$/, '').replace(/^✗ /, '');
|
||||
const out = document.getElementById('pysim-fs-content');
|
||||
const statusEl = document.getElementById('pysim-fs-status');
|
||||
out.innerHTML = '';
|
||||
statusEl.textContent = 'Loading...';
|
||||
try {
|
||||
const node = pysimFsFindNode(name, pysimFsTreeRoot);
|
||||
if (node && node.fid) {
|
||||
await pysimFetch('/api/command', { cmd: 'select ' + node.fid });
|
||||
} else {
|
||||
await pysimFetch('/api/command', { cmd: 'select ' + name });
|
||||
const node = pysimFsFindNode(rawName, pysimFsTreeRoot);
|
||||
let body = { name: rawName, fid: node ? node.fid : null, mode: pysimFsDecodedMode ? 'decoded' : 'raw' };
|
||||
const parentSel = getParentSel(node);
|
||||
if (parentSel) body.parent_sel = parentSel;
|
||||
let data = await pysimFetch('/api/read', body);
|
||||
if (!data.success) {
|
||||
body = { name: rawName, fid: node ? node.fid : null, mode: pysimFsDecodedMode ? 'decoded' : 'raw' };
|
||||
data = await pysimFetch('/api/read', body);
|
||||
}
|
||||
const status = await pysimFetch('/api/status');
|
||||
const ft = status.current_selection?.file_type;
|
||||
const isRecord = ft === 'linear_fixed' || ft === 'cyclic';
|
||||
const cmd = pysimFsDecodedMode
|
||||
? (isRecord ? 'read_records_decoded' : 'read_binary_decoded')
|
||||
: (isRecord ? 'read_records' : 'read_binary');
|
||||
const data = await pysimFetch('/api/command', { cmd });
|
||||
const output = data.output || '';
|
||||
const swMatch = output.match(/SW:\s*(\w+)/);
|
||||
const sw = swMatch ? swMatch[1] : null;
|
||||
const errMatch = output.match(/got (\w+)/);
|
||||
if (errMatch) {
|
||||
const swErr = errMatch[1];
|
||||
const desc = ({'6982':'Security status not satisfied (PIN required?)','6983':'PIN blocked','6985':'Conditions of use not satisfied','6A88':'Referenced data not found','6A82':'File not found'})[swErr] || 'Error';
|
||||
statusEl.textContent = 'SW: ' + swErr + ' — ' + desc;
|
||||
if (!data.success) {
|
||||
statusEl.textContent = 'SW: ' + (data.sw || '?') + ' — ' + (data.error || 'Error');
|
||||
return;
|
||||
}
|
||||
statusEl.textContent = sw === '9000' ? 'SW: 9000 OK' : sw ? 'SW: ' + sw : 'OK';
|
||||
if (pysimFsDecodedMode) {
|
||||
try {
|
||||
const parsed = JSON.parse(output);
|
||||
out.innerHTML = '<pre class="text-xs leading-relaxed">' + esc(JSON.stringify(parsed, null, 2)) + '</pre>';
|
||||
} catch (e) {
|
||||
out.innerHTML = '<textarea rows="8" readonly class="w-full font-mono text-xs border-0 bg-transparent resize-none">' + esc(output) + '</textarea>';
|
||||
statusEl.textContent = 'SW: ' + data.sw + ' OK';
|
||||
if (data.decoded) {
|
||||
out.innerHTML = '<pre class="text-xs leading-relaxed">' + esc(JSON.stringify(data.decoded, null, 2)) + '</pre>';
|
||||
} else if (data.records) {
|
||||
let html = '';
|
||||
for (const r of data.records) {
|
||||
html += '<div class="flex items-center gap-1 mb-1">' +
|
||||
'<span class="text-xs w-8 text-gray-500 dark:text-slate-400 shrink-0">' + r.num + '</span>' +
|
||||
'<input class="flex-1 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 bg-white dark:bg-slate-800 text-gray-800 dark:text-slate-200" style="overflow-x:auto;min-width:100px" value="' + esc(r.data) + '" readonly>' +
|
||||
'</div>';
|
||||
}
|
||||
out.innerHTML = html || '(no records)';
|
||||
} else {
|
||||
const clean = output.replace(/^SW:\s*\w+\s*/m, '').replace(/^SW:\s*\w+\s*/m, '').trim();
|
||||
if (isRecord) {
|
||||
const lines = clean.split('\n');
|
||||
let html = '';
|
||||
for (const line of lines) {
|
||||
const m = line.match(/^(\d+)\s(.+)/);
|
||||
if (m) {
|
||||
html += '<div class="flex items-center gap-1 mb-1">' +
|
||||
'<span class="text-xs w-8 text-gray-500 dark:text-slate-400 shrink-0">' + m[1] + '</span>' +
|
||||
'<input class="flex-1 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 bg-white dark:bg-slate-800 text-gray-800 dark:text-slate-200" value="' + esc(m[2]) + '" readonly>' +
|
||||
'</div>';
|
||||
}
|
||||
}
|
||||
out.innerHTML = html || '(no records)';
|
||||
} else {
|
||||
out.innerHTML = '<textarea rows="8" readonly class="w-full font-mono text-xs border-0 bg-transparent resize-none">' + esc(clean) + '</textarea>';
|
||||
}
|
||||
out.innerHTML = '<textarea rows="8" readonly class="w-full font-mono text-xs border-0 bg-transparent resize-none">' + esc(data.data || '(no output)') + '</textarea>';
|
||||
}
|
||||
} catch (e) {
|
||||
statusEl.textContent = 'Error: ' + e.message;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="183"
|
||||
height="183"
|
||||
viewBox="-56.5 -44 183 183"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xml:space="preserve"
|
||||
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||
sodipodi:docname="sim.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="1.3469697"
|
||||
inkscape:cx="113.2171"
|
||||
inkscape:cy="187.82902"
|
||||
inkscape:window-width="1707"
|
||||
inkscape:window-height="1007"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" /><defs
|
||||
id="defs1" /><g
|
||||
inkscape:label="Слой 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"><path
|
||||
style="fill:#000000"
|
||||
d="m -0.1304947,103.98892 c -2.832044,-0.76743 -5.441699,-3.02673 -6.734761,-5.830592 l -0.600371,-1.30184 V 47.511687 -1.833103 l 0.690582,-1.45521 c 1.069347,-2.25335 2.620862,-3.8111129 4.935149,-4.9550229 l 2.01929,-0.9981 24.2233257,-0.07338 c 15.628147,-0.04735 24.667267,0.02165 25.474467,0.194438 2.93081,0.627379 3.24328,0.889045 14.83492,12.422955 9.75955,9.7109599 11.15853,11.2053299 11.90625,12.7181399 l 0.84727,1.71423 v 39.56077 39.560781 l -0.69058,1.4552 c -1.06939,2.253442 -2.62086,3.811122 -4.93526,4.955032 l -2.01939,0.9981 -34.27739,0.0512 c -29.0101883,0.0433 -34.4919167,-0.007 -35.6735017,-0.32711 z M 69.807477,99.400158 c 1.54279,-0.70076 2.94311,-2.44775 3.28556,-4.09897 0.19262,-0.92877 0.25873,-13.13398 0.21014,-38.793661 -0.0617,-32.61006 -0.12207,-37.5513 -0.46776,-38.31279 -0.58495,-1.28854 -21.30517,-21.96092 -22.62188,-22.56965 -1.00491,-0.46458 -2.26697,-0.48928 -25.003117,-0.48928 -22.6963917,0 -24.0007447,0.0254 -25.0178957,0.48742 -1.533296,0.69645 -2.941608,2.44785 -3.296415,4.09949004 -0.20721,0.96458 -0.267588,14.95269996 -0.209699,48.58271996 l 0.0813,47.228131 0.571082,1.05833 c 0.314094,0.58209 0.909407,1.37919 1.322916,1.77134 1.695763,1.60819 -0.536882,1.51482 36.3382887,1.51979 32.21189,0.004 33.78281,-0.0175 34.80748,-0.48287 z M 15.146229,88.018118 c -2.69069,-0.54101 -5.08953,-2.61032 -6.0729303,-5.23871 -0.53439,-1.42827 -0.54303,-1.82376 -0.47185,-21.602461 l 0.0725,-20.14755 0.77957,-1.40728 c 0.9491803,-1.71346 2.1625903,-2.81514 4.1031503,-3.72534 l 1.46728,-0.68821 h 19.976048 19.97604 l 1.46728,0.68821 c 1.94057,0.9102 3.15397,2.01188 4.10315,3.72534 l 0.77957,1.40728 0.0725,20.14755 c 0.0712,19.778701 0.0625,20.174191 -0.47184,21.602461 -0.72128,1.92779 -2.23869,3.6008 -4.13783,4.56212 l -1.54825,0.78371 -19.57917,0.0405 c -10.768537,0.0223 -20.000388,-0.0442 -20.515228,-0.14766 z m 9.535021,-10.07934 v -5.82084 h -5.820831 -5.82084 v 4.60991 4.60991 l 0.71449,0.84912 c 1.16572,1.38538 1.8679,1.55091 6.6277,1.56237 l 4.299481,0.0104 z m 16.139577,0.01 v -5.811 l -5.82107,-0.076 -5.821067,-0.076 2.4e-4,5.88698 2.4e-4,5.88698 h 5.820827 5.82083 z m 13.96991,5.43925 c 0.39296,-0.20445 1.01213,-0.68997 1.37593,-1.07893 0.64718,-0.69193 0.66325,-0.80958 0.7443,-5.4491 l 0.0829,-4.7419 -5.04378,-0.0331 c -2.77409,-0.0182 -5.22238,-0.0331 -5.44066,-0.0331 -1.24803,0 -1.19063,-0.28384 -1.19063,5.88698 v 5.82083 h 4.37877 c 3.24522,0 4.56372,-0.0962 5.09322,-0.37174 z m 2.16968,-21.720971 v -5.95312 H 41.470267 25.98011 l -0.64943,-0.64943 -0.64943,-0.64944 v -7.42036 -7.42036 h -4.241831 c -4.6105,0 -5.44266,0.18168 -6.60609,1.44224 l -0.66145,0.71669 -0.0734,12.94345 -0.0734,12.94346 h 21.967688 21.96769 z m -16.13959,-16.13974 v -5.95313 h -5.82083 -5.820827 v 5.77674 c 0,3.17721 0.0794,5.85611 0.17639,5.95313 0.097,0.097 2.716387,0.17638 5.820827,0.17638 h 5.64444 z m 16.13959,1.36204 c 0,-4.33264 -0.0338,-4.64169 -0.60059,-5.4901 -1.07012,-1.60187 -1.89152,-1.82419 -6.7416,-1.82465 l -4.29948,-4.2e-4 v 5.77674 c 0,3.17721 0.0794,5.85611 0.17639,5.95313 0.097,0.097 2.71639,0.17638 5.82083,0.17638 h 5.64445 z"
|
||||
id="path1" /></g></svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="183"
|
||||
height="183"
|
||||
viewBox="-56.5 -44 183 183"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xml:space="preserve"
|
||||
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||
sodipodi:docname="sim.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="1.3469697"
|
||||
inkscape:cx="113.2171"
|
||||
inkscape:cy="187.82902"
|
||||
inkscape:window-width="1707"
|
||||
inkscape:window-height="1007"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" /><defs
|
||||
id="defs1" /><g
|
||||
inkscape:label="Слой 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"><animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
from="0 35 47.5"
|
||||
to="360 35 47.5"
|
||||
dur="2s"
|
||||
repeatCount="indefinite" /><path
|
||||
style="fill:#000000"
|
||||
d="m -0.1304947,103.98892 c -2.832044,-0.76743 -5.441699,-3.02673 -6.734761,-5.830592 l -0.600371,-1.30184 V 47.511687 -1.833103 l 0.690582,-1.45521 c 1.069347,-2.25335 2.620862,-3.8111129 4.935149,-4.9550229 l 2.01929,-0.9981 24.2233257,-0.07338 c 15.628147,-0.04735 24.667267,0.02165 25.474467,0.194438 2.93081,0.627379 3.24328,0.889045 14.83492,12.422955 9.75955,9.7109599 11.15853,11.2053299 11.90625,12.7181399 l 0.84727,1.71423 v 39.56077 39.560781 l -0.69058,1.4552 c -1.06939,2.253442 -2.62086,3.811122 -4.93526,4.955032 l -2.01939,0.9981 -34.27739,0.0512 c -29.0101883,0.0433 -34.4919167,-0.007 -35.6735017,-0.32711 z M 69.807477,99.400158 c 1.54279,-0.70076 2.94311,-2.44775 3.28556,-4.09897 0.19262,-0.92877 0.25873,-13.13398 0.21014,-38.793661 -0.0617,-32.61006 -0.12207,-37.5513 -0.46776,-38.31279 -0.58495,-1.28854 -21.30517,-21.96092 -22.62188,-22.56965 -1.00491,-0.46458 -2.26697,-0.48928 -25.003117,-0.48928 -22.6963917,0 -24.0007447,0.0254 -25.0178957,0.48742 -1.533296,0.69645 -2.941608,2.44785 -3.296415,4.09949004 -0.20721,0.96458 -0.267588,14.95269996 -0.209699,48.58271996 l 0.0813,47.228131 0.571082,1.05833 c 0.314094,0.58209 0.909407,1.37919 1.322916,1.77134 1.695763,1.60819 -0.536882,1.51482 36.3382887,1.51979 32.21189,0.004 33.78281,-0.0175 34.80748,-0.48287 z M 15.146229,88.018118 c -2.69069,-0.54101 -5.08953,-2.61032 -6.0729303,-5.23871 -0.53439,-1.42827 -0.54303,-1.82376 -0.47185,-21.602461 l 0.0725,-20.14755 0.77957,-1.40728 c 0.9491803,-1.71346 2.1625903,-2.81514 4.1031503,-3.72534 l 1.46728,-0.68821 h 19.976048 19.97604 l 1.46728,0.68821 c 1.94057,0.9102 3.15397,2.01188 4.10315,3.72534 l 0.77957,1.40728 0.0725,20.14755 c 0.0712,19.778701 0.0625,20.174191 -0.47184,21.602461 -0.72128,1.92779 -2.23869,3.6008 -4.13783,4.56212 l -1.54825,0.78371 -19.57917,0.0405 c -10.768537,0.0223 -20.000388,-0.0442 -20.515228,-0.14766 z m 9.535021,-10.07934 v -5.82084 h -5.820831 -5.82084 v 4.60991 4.60991 l 0.71449,0.84912 c 1.16572,1.38538 1.8679,1.55091 6.6277,1.56237 l 4.299481,0.0104 z m 16.139577,0.01 v -5.811 l -5.82107,-0.076 -5.821067,-0.076 2.4e-4,5.88698 2.4e-4,5.88698 h 5.820827 5.82083 z m 13.96991,5.43925 c 0.39296,-0.20445 1.01213,-0.68997 1.37593,-1.07893 0.64718,-0.69193 0.66325,-0.80958 0.7443,-5.4491 l 0.0829,-4.7419 -5.04378,-0.0331 c -2.77409,-0.0182 -5.22238,-0.0331 -5.44066,-0.0331 -1.24803,0 -1.19063,-0.28384 -1.19063,5.88698 v 5.82083 h 4.37877 c 3.24522,0 4.56372,-0.0962 5.09322,-0.37174 z m 2.16968,-21.720971 v -5.95312 H 41.470267 25.98011 l -0.64943,-0.64943 -0.64943,-0.64944 v -7.42036 -7.42036 h -4.241831 c -4.6105,0 -5.44266,0.18168 -6.60609,1.44224 l -0.66145,0.71669 -0.0734,12.94345 -0.0734,12.94346 h 21.967688 21.96769 z m -16.13959,-16.13974 v -5.95313 h -5.82083 -5.820827 v 5.77674 c 0,3.17721 0.0794,5.85611 0.17639,5.95313 0.097,0.097 2.716387,0.17638 5.820827,0.17638 h 5.64444 z m 16.13959,1.36204 c 0,-4.33264 -0.0338,-4.64169 -0.60059,-5.4901 -1.07012,-1.60187 -1.89152,-1.82419 -6.7416,-1.82465 l -4.29948,-4.2e-4 v 5.77674 c 0,3.17721 0.0794,5.85611 0.17639,5.95313 0.097,0.097 2.71639,0.17638 5.82083,0.17638 h 5.64445 z"
|
||||
id="path1" /></g></svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
@@ -1041,6 +1041,10 @@ video {
|
||||
color: rgb(202 138 4 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
.filter {
|
||||
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ self.addEventListener('activate', e => {
|
||||
|
||||
self.addEventListener('fetch', e => {
|
||||
if (!e.request.url.startsWith('http')) return;
|
||||
if (e.request.method !== 'GET') return;
|
||||
const isNavigate = e.request.mode === 'navigate' || e.request.url.endsWith('sw.js');
|
||||
if (isNavigate) {
|
||||
e.respondWith(
|
||||
|
||||
Reference in New Issue
Block a user