-
OTAMan SIM OTA with a Human Face v1.0.0
+
OTAMan SIM OTA with a Human Face v1.1.0
github
@@ -845,6 +845,7 @@
+
@@ -870,12 +871,23 @@
+
+
Add known file paths and aliases to the file tree.
+
+
+
+
+
+
+
+
@@ -2640,10 +2652,11 @@ function pysimSwitchSubtab(name) {
btn.classList.toggle('text-gray-700', !isActive);
btn.classList.toggle('dark:text-slate-300', !isActive);
});
- ['cmd', 'apdu', 'files'].forEach(s => {
+ ['cmd', 'apdu', 'files', 'custom'].forEach(s => {
document.getElementById('pysim-sub-' + s).classList.toggle('hidden', s !== name);
});
if (name === 'files') pysimFsRefresh();
+ if (name === 'custom') pysimCustomRender();
}
function pysimCmdKeydown(e) {
@@ -2654,6 +2667,7 @@ function pysimCmdKeydown(e) {
pysimCmdHistIdx = pysimCmdHistory.length;
}
pysimExec();
+ document.getElementById('pysim-cmd-hint').classList.add('hidden');
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (pysimCmdHistIdx > 0) {
@@ -2672,6 +2686,33 @@ function pysimCmdKeydown(e) {
}
}
+let pysimCmdHintTimeout = null;
+
+async function pysimCmdHint() {
+ clearTimeout(pysimCmdHintTimeout);
+ pysimCmdHintTimeout = setTimeout(async () => {
+ const input = document.getElementById('pysim-cmd');
+ const text = input.value.trim();
+ const cmd = text.split(/\s+/)[0];
+ if (!cmd || text.includes(' ')) {
+ document.getElementById('pysim-cmd-hint').classList.add('hidden');
+ return;
+ }
+ const hintEl = document.getElementById('pysim-cmd-hint');
+ try {
+ const data = await pysimFetch('/api/help', { cmd });
+ if (data.usage) {
+ hintEl.textContent = data.usage;
+ hintEl.classList.remove('hidden');
+ } else {
+ hintEl.classList.add('hidden');
+ }
+ } catch (e) {
+ hintEl.classList.add('hidden');
+ }
+ }, 300);
+}
+
async function pysimLoadCommands() {
const list = document.getElementById('pysim-cmdlist');
try {
@@ -2694,6 +2735,31 @@ function getParentSel(node) {
return p.name;
}
+function pysimCustomInject(node) {
+ if (!node || !node.fid) return;
+ const pfid = node.fid.toLowerCase();
+ for (const c of pysimCustomFiles) {
+ if (c.parentFid.toLowerCase() !== pfid) continue;
+ const existing = (node.children || []).find(ch => ch.fid === c.fid);
+ if (existing) {
+ existing.name = c.name;
+ existing.custom = true;
+ } else {
+ node.children = node.children || [];
+ node.children.push({
+ name: c.name,
+ fid: c.fid,
+ isDir: c.name.startsWith('DF.') || c.name.startsWith('ADF.'),
+ children: null,
+ expanded: false,
+ exists: null,
+ custom: true,
+ parent: node,
+ });
+ }
+ }
+}
+
async function pysimFsRefresh() {
const detailEl = document.getElementById('pysim-fs-detail');
detailEl.classList.add('hidden');
@@ -2718,6 +2784,7 @@ async function pysimFsRefresh() {
parent: pysimFsTreeRoot,
}));
pysimFsTreeRoot.exists = true;
+ pysimCustomInject(pysimFsTreeRoot);
pysimFsRenderTree();
}
@@ -2746,6 +2813,7 @@ async function pysimFsLoadChildren(node) {
parent: node,
}));
node.exists = true;
+ pysimCustomInject(node);
} catch (e) {
node.exists = false;
node.children = [];
@@ -2764,26 +2832,28 @@ function pysimFsRenderNode(node, depth) {
const pad = ' '.repeat(depth * 4);
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 verified = node.exists !== null;
+ const color = verified ? (exists ? (isDir ? 'text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-slate-300') : 'text-red-400 dark:text-red-400') : 'text-gray-500 dark:text-slate-400';
+ const italic = node.custom ? ' italic' : '';
const fidStr = node.fid ? '
(' + node.fid.toUpperCase() + ')' : '';
const aidStr = node.aid ? '
[' + node.aid + ']' : '';
- let html = '
';
+ let html = '
';
html += pad;
- if (!exists) {
+ if (verified && !exists) {
html += '✗ ';
}
if (isDir) {
- if (exists) {
+ if ((verified && exists) || !verified) {
const toggle = node.children && node.expanded ? '▼' : '▶';
- html += '' + toggle + ' ';
- html += '' + esc(node.name) + fidStr + aidStr + '';
+ html += '' + toggle + ' ';
+ html += '' + esc(node.name) + fidStr + aidStr + '';
} else {
html += esc(node.name) + fidStr + aidStr + '
';
return html;
}
} else {
- html += '
' + esc(node.name) + fidStr + aidStr + '';
+ html += '
' + esc(node.name) + fidStr + aidStr + '';
}
html += '
';
if (isDir && node.children && node.expanded) {
@@ -2915,6 +2985,61 @@ function pysimFsSetMode(mode) {
});
}
+// ===== custom files =====
+let pysimCustomFiles = [];
+
+function pysimCustomLoad() {
+ try {
+ const d = localStorage.getItem('pysim_custom_files');
+ pysimCustomFiles = d ? JSON.parse(d) : [];
+ } catch (e) { pysimCustomFiles = []; }
+}
+
+function pysimCustomSave() {
+ localStorage.setItem('pysim_custom_files', JSON.stringify(pysimCustomFiles));
+}
+
+function pysimCustomAdd() {
+ const pathEl = document.getElementById('pysim-cf-path');
+ const nameEl = document.getElementById('pysim-cf-name');
+ const path = pathEl.value.trim().toUpperCase();
+ const name = nameEl.value.trim().toUpperCase();
+ if (!path || !name) return;
+ const parts = path.split('/').filter(Boolean);
+ if (parts.length < 2) { alert('Path must have at least 2 parts, e.g. 3F00/6F46'); return; }
+ const fid = parts[parts.length - 1];
+ const parentFid = parts[parts.length - 2];
+ if (pysimCustomFiles.some(c => c.path === path)) { alert('Path already exists'); return; }
+ pysimCustomFiles.push({ path, name, fid, parentFid });
+ pysimCustomSave();
+ pysimCustomRender();
+ pathEl.value = '';
+ nameEl.value = '';
+}
+
+function pysimCustomRemove(i) {
+ pysimCustomFiles.splice(i, 1);
+ pysimCustomSave();
+ pysimCustomRender();
+}
+
+function pysimCustomRender() {
+ const el = document.getElementById('pysim-cf-list');
+ if (!pysimCustomFiles.length) { el.innerHTML = '
No custom files defined.'; return; }
+ let html = '';
+ for (let i = 0; i < pysimCustomFiles.length; i++) {
+ const c = pysimCustomFiles[i];
+ html += '
';
+ html += '' + esc(c.path) + ' → ' + esc(c.name) + '';
+ html += '✕';
+ html += '
';
+ }
+ el.innerHTML = html;
+}
+
+// Init custom files
+pysimCustomLoad();
+
// Init sub-tab pills
document.querySelectorAll('.pysim-subtab').forEach(btn => {
btn.addEventListener('click', () => pysimSwitchSubtab(btn.dataset.pysimSub));
diff --git a/style.css b/style.css
index ee1dadd..2487bb2 100644
--- a/style.css
+++ b/style.css
@@ -599,6 +599,10 @@ video {
margin-left: 2rem;
}
+.ml-auto {
+ margin-left: auto;
+}
+
.mr-1 {
margin-right: 0.25rem;
}
@@ -663,6 +667,10 @@ video {
width: 6rem;
}
+.w-48 {
+ width: 12rem;
+}
+
.w-6 {
width: 1.5rem;
}
@@ -769,6 +777,10 @@ video {
overflow-y: auto;
}
+.whitespace-nowrap {
+ white-space: nowrap;
+}
+
.whitespace-pre-wrap {
white-space: pre-wrap;
}
@@ -802,6 +814,11 @@ video {
border-top-width: 1px;
}
+.border-gray-200 {
+ --tw-border-opacity: 1;
+ border-color: rgb(229 231 235 / var(--tw-border-opacity, 1));
+}
+
.border-gray-300 {
--tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
@@ -962,6 +979,10 @@ video {
font-weight: 400;
}
+.italic {
+ font-style: italic;
+}
+
.leading-relaxed {
line-height: 1.625;
}
@@ -1073,6 +1094,11 @@ video {
color: rgb(75 85 99 / var(--tw-text-opacity, 1));
}
+.hover\:text-red-600:hover {
+ --tw-text-opacity: 1;
+ color: rgb(220 38 38 / var(--tw-text-opacity, 1));
+}
+
.hover\:text-red-700:hover {
--tw-text-opacity: 1;
color: rgb(185 28 28 / var(--tw-text-opacity, 1));
@@ -1099,6 +1125,11 @@ video {
border-color: rgb(71 85 105 / var(--tw-border-opacity, 1));
}
+.dark\:border-slate-700:is(.dark *) {
+ --tw-border-opacity: 1;
+ border-color: rgb(51 65 85 / var(--tw-border-opacity, 1));
+}
+
.dark\:bg-blue-500:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1));