ui: probe-all-files walk and honest file tree coloring

- pysimFsLoadChildren now treats {success:false,error} (and any error
  payload) as a failed listing: absent DFs/ADFs render as a red cross
  without an expand arrow instead of silently opening empty; the
  /api/tree 500 body also carries exists:false for shape consistency
  with /api/select
- an expanded directory with zero children shows a gray (empty)
  placeholder
- new Probe all files button (data-needs=card) in the tree header:
  walks the whole model tree from MF, verifies every DF/ADF via
  /api/tree and every file (incl. custom entries) via /api/select,
  skips subtrees of absent dirs, shows N/total progress, toggles to
  Stop, and reports present/absent counts with elapsed time; results
  colour the current tree only
- tests: fs_load (error/retry/empty), fs_render (red cross, (empty)),
  fs_probe (flags, absent-dir skip, custom files, stop, summary);
  html structural check; help EN/RU, READMEs, AGENTS updated;
  SW cache v115 -> v116.
This commit is contained in:
2026-09-12 21:14:09 +03:00
parent 4598a70db2
commit 92271d6726
11 changed files with 394 additions and 6 deletions
+2
View File
@@ -490,6 +490,8 @@ Browse the UICC filesystem in a tree view. Files are shown with names, FIDs, and
- **Read** — reads the selected file (auto-detects transparent vs record files) - **Read** — reads the selected file (auto-detects transparent vs record files)
- **Edit** — switch to edit mode, modify hex data, click **Save** to write back - **Edit** — switch to edit mode, modify hex data, click **Save** to write back
- **Raw / Decoded** — toggle between hex dump and pysim-decoded JSON view - **Raw / Decoded** — toggle between hex dump and pysim-decoded JSON view
- Missing files are shown in red (✗); a present but empty DF shows `(empty)`
- **Probe all files** — walks the whole tree (incl. custom files), marks every entry present/absent with *N / total* progress, stoppable, and ends with a summary; browsing itself stays lazy
### Command Hints ### Command Hints
+2
View File
@@ -464,6 +464,8 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
- **Read** — чтение файла (автоопределение transparent/record) - **Read** — чтение файла (автоопределение transparent/record)
- **Edit** — режим редактирования, измените hex-данные и нажмите **Save** для записи - **Edit** — режим редактирования, измените hex-данные и нажмите **Save** для записи
- **Raw / Decoded** — переключение между hex-дампом и декодированным JSON - **Raw / Decoded** — переключение между hex-дампом и декодированным JSON
- Отсутствующие файлы показаны красным (✗); существующий пустой DF — `(пусто)`
- **Проверить все файлы** — обход всего дерева (включая пользовательские) с пометкой «есть/нет», прогрессом *N / всего*, возможностью остановки и сводкой в конце; сам просмотр остаётся ленивым
### Подсказки команд ### Подсказки команд
+1
View File
@@ -322,6 +322,7 @@
<li><strong>Прочитать</strong> — чтение файла (автоопределение transparent/record)</li> <li><strong>Прочитать</strong> — чтение файла (автоопределение transparent/record)</li>
<li><strong>Редактировать</strong> — изменение hex-данных, <strong>Сохранить</strong> для записи (или <strong>Отмена</strong>)</li> <li><strong>Редактировать</strong> — изменение hex-данных, <strong>Сохранить</strong> для записи (или <strong>Отмена</strong>)</li>
<li><strong>Данные как на карте / Декодированные данные</strong> — переключение между hex-дампом и декодированным JSON</li> <li><strong>Данные как на карте / Декодированные данные</strong> — переключение между hex-дампом и декодированным JSON</li>
<li><strong>Проверить все файлы</strong> — обход всего дерева (включая пользовательские файлы) с пометкой каждого элемента: есть (обычный вид) или нет (красный ✗, без стрелки разворачивания); существующие пустые DF показывают <code class="font-mono text-sm">(пусто)</code>. Отображается прогресс <em>N / всего</em>, обход можно остановить; в конце — сводка «есть/нет». Файлы проверяются только при разворачивании или проверке — просмотр остаётся ленивым.</li>
</ul> </ul>
<h3 id="pysim-cmdline" class="text-lg font-medium mb-2">4.2 Командная строка pySim</h3> <h3 id="pysim-cmdline" class="text-lg font-medium mb-2">4.2 Командная строка pySim</h3>
+1
View File
@@ -322,6 +322,7 @@
<li><strong>Read</strong> — reads the selected file (auto-detects transparent vs record files)</li> <li><strong>Read</strong> — reads the selected file (auto-detects transparent vs record files)</li>
<li><strong>Edit</strong> — modify hex data, <strong>Save</strong> to write back (or <strong>Cancel</strong>)</li> <li><strong>Edit</strong> — modify hex data, <strong>Save</strong> to write back (or <strong>Cancel</strong>)</li>
<li><strong>Raw / Decoded</strong> — toggle between hex dump and pySim-decoded JSON</li> <li><strong>Raw / Decoded</strong> — toggle between hex dump and pySim-decoded JSON</li>
<li><strong>Probe all files</strong> — walks the whole tree (including custom files) and marks every entry present (normal) or absent (red ✗, no expand arrow); empty-but-present DFs show <code class="font-mono text-sm">(empty)</code>. Shows progress <em>N / total</em>, can be stopped, and finishes with a present/absent summary. Files are only verified when expanded or probed — browsing stays lazy.</li>
</ul> </ul>
<h3 id="pysim-cmdline" class="text-lg font-medium mb-2">4.2 pySim command line</h3> <h3 id="pysim-cmdline" class="text-lg font-medium mb-2">4.2 pySim command line</h3>
+101 -4
View File
@@ -860,6 +860,8 @@
<span class="pysim-fs-sort-pill px-2 py-0.5 rounded cursor-pointer bg-blue-600 text-white" data-fs-sort="fid" onclick="pysimFsSetSort('fid')">FID</span> <span class="pysim-fs-sort-pill px-2 py-0.5 rounded cursor-pointer bg-blue-600 text-white" data-fs-sort="fid" onclick="pysimFsSetSort('fid')">FID</span>
<span class="pysim-fs-sort-pill px-2 py-0.5 rounded cursor-pointer bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300" data-fs-sort="name" onclick="pysimFsSetSort('name')" data-l10n="Name">Name</span> <span class="pysim-fs-sort-pill px-2 py-0.5 rounded cursor-pointer bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300" data-fs-sort="name" onclick="pysimFsSetSort('name')" data-l10n="Name">Name</span>
</div> </div>
<button id="pysim-fs-probe-btn" data-needs="card" onclick="pysimFsProbeAll()" class="w-full mb-1 px-2 py-0.5 rounded bg-gray-600 text-white hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Probe all files">Probe all files</button>
<div id="pysim-fs-probe-status" class="hidden mb-1 text-gray-500 dark:text-slate-400"></div>
<div id="pysim-fs-tree"></div> <div id="pysim-fs-tree"></div>
</div> </div>
<div class="flex-1"> <div class="flex-1">
@@ -5946,8 +5948,8 @@ async function pysimFsLoadChildren(node) {
body = { name: node.name, fid: node.fid }; body = { name: node.name, fid: node.fid };
data = await pysimFetch('/api/tree', body); data = await pysimFetch('/api/tree', body);
} }
if (!data || data.exists === false) { if (!data || data.exists === false || data.success === false || data.error) {
throw new Error('File not found'); throw new Error((data && data.error) || 'File not found');
} }
node.children = (data.children || []).map(c => ({ node.children = (data.children || []).map(c => ({
name: c.name, name: c.name,
@@ -5968,6 +5970,86 @@ async function pysimFsLoadChildren(node) {
} }
} }
let _pysimFsProbe = null;
function pysimFsSelectBody(node) {
const body = { name: node.name, fid: node.fid };
const parentSel = getParentSel(node);
if (parentSel) body.parent_sel = parentSel;
return body;
}
function pysimFsProbeUi(text, stopLabel) {
const status = document.getElementById('pysim-fs-probe-status');
if (status) {
status.textContent = text;
status.classList.toggle('hidden', !text);
}
const btn = document.getElementById('pysim-fs-probe-btn');
if (btn) {
btn.textContent = stopLabel ? t('Stop') : t('Probe all files');
btn.setAttribute('data-l10n', stopLabel ? 'Stop' : 'Probe all files');
}
}
async function pysimFsProbeAll() {
if (_pysimFsProbe) { _pysimFsProbe.stop = true; return; }
if (!pysimFsTreeRoot) return;
const probe = { stop: false, dirs: 0, done: 0, total: 0, present: 0, absent: 0 };
_pysimFsProbe = probe;
const started = Date.now();
let error = null;
const count = exists => { if (exists) probe.present++; else probe.absent++; };
try {
if (!pysimFsTreeRoot.children) await pysimFsLoadChildren(pysimFsTreeRoot);
const discover = async node => {
for (const child of pysimFsSortChildren(node.children, pysimFsSort)) {
if (probe.stop) return;
if (!child.isDir) continue;
await pysimFsLoadChildren(child);
const exists = child.exists !== false;
probe.dirs++;
count(exists);
pysimFsRenderTree();
pysimFsProbeUi(t('Probing:') + ' ' + t('discovering') + ' (' + probe.dirs + ')', true);
if (exists) await discover(child);
}
};
await discover(pysimFsTreeRoot);
const files = [];
const collect = node => {
for (const child of (node.children || [])) {
if (child.isDir) { if (child.exists !== false) collect(child); }
else files.push(child);
}
};
collect(pysimFsTreeRoot);
probe.total = files.length + probe.dirs;
probe.done = probe.dirs;
for (const file of files) {
if (probe.stop) break;
const data = await pysimFetch('/api/select', pysimFsSelectBody(file));
const exists = !!(data && data.exists !== false && !data.error);
file.exists = exists;
if (!exists) file.children = [];
count(exists);
probe.done++;
pysimFsProbeUi(t('Probing:') + ' ' + probe.done + '/' + probe.total, true);
if (probe.done % 10 === 0 || probe.done === probe.total) pysimFsRenderTree();
}
} catch (e) {
error = (e && e.message) ? e.message : String(e);
}
pysimFsRenderTree();
const secs = ((Date.now() - started) / 1000).toFixed(1);
let summary = t('Probed:') + ' ' + probe.done + '/' + probe.total + ' ' + t('files') + ' — '
+ probe.present + ' ' + t('present') + ', ' + probe.absent + ' ' + t('absent') + ' (' + secs + 's)';
if (error) summary = t('Error') + ': ' + error + ' — ' + summary;
else if (probe.stop) summary = t('Stopped') + ' — ' + summary;
_pysimFsProbe = null;
pysimFsProbeUi(summary, false);
}
function pysimFsRenderTree() { function pysimFsRenderTree() {
const el = document.getElementById('pysim-fs-tree'); const el = document.getElementById('pysim-fs-tree');
if (!pysimFsTreeRoot) { el.innerHTML = '(loading)'; return; } if (!pysimFsTreeRoot) { el.innerHTML = '(loading)'; return; }
@@ -6004,8 +6086,12 @@ function pysimFsRenderNode(node, depth) {
} }
html += '</div>'; html += '</div>';
if (isDir && node.children && node.expanded) { if (isDir && node.children && node.expanded) {
for (const child of pysimFsSortChildren(node.children, pysimFsSort)) { if (!node.children.length) {
html += pysimFsRenderNode(child, depth + 1); html += '<div class="text-gray-400 dark:text-slate-500">' + pad + '&nbsp;&nbsp;&nbsp;&nbsp;' + esc(t('(empty)')) + '</div>';
} else {
for (const child of pysimFsSortChildren(node.children, pysimFsSort)) {
html += pysimFsRenderNode(child, depth + 1);
}
} }
} }
return html; return html;
@@ -9147,6 +9233,17 @@ const LANG_RU = {
'Phone simulator': 'Симулятор телефона', 'Phone simulator': 'Симулятор телефона',
'Sort:': 'Сортировка:', 'Sort:': 'Сортировка:',
'Name': 'Имя', 'Name': 'Имя',
'Probe all files': 'Проверить все файлы',
'Stop': 'Остановить',
'Probing:': 'Проверка:',
'discovering': 'поиск файлов',
'files': 'файлов',
'present': 'есть',
'absent': 'нет',
'Probed:': 'Проверено:',
'Stopped': 'Остановлено',
'(empty)': '(пусто)',
'Error': 'Ошибка',
'No server connection': 'Нет соединения с сервером', 'No server connection': 'Нет соединения с сервером',
'Server connected, no card equipped': 'Сервер подключён, карта не подключена', 'Server connected, no card equipped': 'Сервер подключён, карта не подключена',
'Card equipped': 'Карта подключена', 'Card equipped': 'Карта подключена',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'otaman-v115'; const CACHE = 'otaman-v116';
const URLS = [ const URLS = [
'index.html', 'index.html',
'help.html', 'help.html',
+96
View File
@@ -0,0 +1,96 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name) {
const re = new RegExp('(?:async\\s+)?function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
}
let code = '';
code += extractFunc(html, 'getParentSel') + '\n';
code += extractFunc(html, 'pysimFsLoadChildren') + '\n';
code += 'globalThis.pysimCustomInject = () => {};\n';
eval(code);
let calls = [];
let responses = [];
let renders = 0;
function setup() {
calls = [];
responses = [];
renders = 0;
globalThis.pysimFetch = async (p, body) => {
calls.push({ path: p, body: JSON.parse(JSON.stringify(body)) });
const r = responses.shift();
if (r instanceof Error) throw r;
return JSON.parse(JSON.stringify(r));
};
globalThis.pysimFsRenderTree = () => { renders++; };
const node = { name: 'DF.USIM', fid: '7fff', isDir: true, children: null, exists: null, parent: { name: 'MF', fid: '3f00' } };
return node;
}
test('tree error payload marks the directory as absent', async () => {
const node = setup();
responses = [
{ success: false, error: 'SW ... 6a82', exists: false },
{ success: false, error: 'SW ... 6a82', exists: false },
];
await pysimFsLoadChildren(node);
assert.strictEqual(node.exists, false);
assert.deepStrictEqual(node.children, []);
assert.strictEqual(renders, 1);
assert.strictEqual(calls.length, 2);
assert.strictEqual(calls[0].body.parent_sel, 'MF');
});
test('error payload without exists is not treated as an empty listing', async () => {
const node = setup();
responses = [
{ success: false, error: 'boom' },
{ success: false, error: 'boom' },
];
await pysimFsLoadChildren(node);
assert.strictEqual(node.exists, false);
assert.deepStrictEqual(node.children, []);
assert.strictEqual(renders, 1);
assert.strictEqual(calls.length, 1);
});
test('retry without parent_sel succeeds and maps children', async () => {
const node = setup();
responses = [
{ exists: false },
{ exists: true, children: [{ name: 'EF.IMSI', fid: '6f07', isDir: false }] },
];
await pysimFsLoadChildren(node);
assert.strictEqual(node.exists, true);
assert.strictEqual(node.children.length, 1);
assert.strictEqual(node.children[0].parent, node);
assert.strictEqual(node.children[0].exists, true);
assert.strictEqual(calls[1].body.parent_sel, undefined);
});
test('empty successful listing keeps the directory present', async () => {
const node = setup();
responses = [{ exists: true, children: [] }];
await pysimFsLoadChildren(node);
assert.strictEqual(node.exists, true);
assert.deepStrictEqual(node.children, []);
});
+124
View File
@@ -0,0 +1,124 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name) {
const re = new RegExp('(?:async\\s+)?function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
}
let code = 'var pysimFsTreeRoot = null;\nvar _pysimFsProbe = null;\nvar pysimFsSort = "fid";\n';
for (const fn of ['getParentSel', 'pysimFsSortChildren', 'pysimFsLoadChildren', 'pysimFsSelectBody', 'pysimFsProbeUi', 'pysimFsProbeAll']) {
code += extractFunc(html, fn) + '\n';
}
code += 'globalThis.esc = s => s;\nglobalThis.t = s => s;\nglobalThis.pysimCustomInject = () => {};\n';
eval(code);
function fakeEl() {
const classes = new Set();
return {
textContent: '',
attrs: {},
classList: {
add: (...cs) => cs.forEach(c => classes.add(c)),
remove: (...cs) => cs.forEach(c => classes.delete(c)),
contains: c => classes.has(c),
toggle: (c, on) => { if (on === undefined ? !classes.has(c) : on) classes.add(c); else classes.delete(c); },
},
setAttribute(k, v) { this.attrs[k] = v; },
};
}
let els = {};
let calls = [];
function setup(routes) {
els = { 'pysim-fs-probe-status': fakeEl(), 'pysim-fs-probe-btn': fakeEl() };
calls = [];
globalThis.document = { getElementById: id => els[id] || null };
globalThis.pysimFetch = async (p, body) => {
calls.push({ path: p, body: body || {} });
for (const r of routes) {
if (r.path !== p) continue;
if (r.name && (!body || body.name !== r.name)) continue;
return typeof r.reply === 'function' ? r.reply(body, calls) : JSON.parse(JSON.stringify(r.reply));
}
throw new Error('unexpected fetch ' + p + ' ' + JSON.stringify(body));
};
globalThis.pysimFsRenderTree = () => {};
}
function root(children) {
pysimFsTreeRoot = { name: 'MF', fid: '3f00', isDir: true, expanded: true, exists: true, children: null, parent: null };
pysimFsTreeRoot.children = children.map(c => Object.assign({ children: null, expanded: false, exists: true, parent: pysimFsTreeRoot }, c));
return pysimFsTreeRoot;
}
const df = (name, fid) => ({ name, fid, isDir: true });
const ef = (name, fid) => ({ name, fid, isDir: false });
const selectNames = () => calls.filter(c => c.path === '/api/select').map(c => c.body.name);
test('probes dirs and files, including custom entries, and reports counts', async () => {
root([df('DF.A', '5f01'), ef('EF.ROOT', '2f01'), Object.assign(ef('EF.CUSTOM', '6fcc'), { custom: true })]);
let sawStop = null;
setup([
{ path: '/api/tree', name: 'DF.A', reply: { exists: true, children: [{ name: 'EF.1', fid: '6f01', isDir: false }] } },
{ path: '/api/select', name: 'EF.1', reply: () => { sawStop = els['pysim-fs-probe-btn'].textContent; return { exists: true }; } },
{ path: '/api/select', name: 'EF.ROOT', reply: { error: 'SW 6a82', exists: false } },
{ path: '/api/select', name: 'EF.CUSTOM', reply: { exists: true } },
]);
await pysimFsProbeAll();
assert.strictEqual(sawStop, 'Stop');
assert.deepStrictEqual(selectNames(), ['EF.1', 'EF.ROOT', 'EF.CUSTOM']);
assert.strictEqual(pysimFsTreeRoot.children[0].children[0].exists, true);
assert.strictEqual(pysimFsTreeRoot.children[1].exists, false);
assert.strictEqual(pysimFsTreeRoot.children[2].exists, true);
const status = els['pysim-fs-probe-status'].textContent;
assert.match(status, /4\/4 files/);
assert.match(status, /3 present/);
assert.match(status, /1 absent/);
assert.strictEqual(els['pysim-fs-probe-btn'].textContent, 'Probe all files');
assert.strictEqual(els['pysim-fs-probe-btn'].attrs['data-l10n'], 'Probe all files');
});
test('an absent directory is marked and its subtree is never fetched', async () => {
root([df('DF.B', '5f02'), ef('EF.ROOT', '2f01')]);
setup([
{ path: '/api/tree', name: 'DF.B', reply: { success: false, error: 'SW 6a82', exists: false } },
{ path: '/api/select', name: 'EF.ROOT', reply: { error: 'SW 6a82', exists: false } },
]);
await pysimFsProbeAll();
assert.strictEqual(pysimFsTreeRoot.children[0].exists, false);
assert.deepStrictEqual(selectNames(), ['EF.ROOT']);
assert.strictEqual(calls.filter(c => c.path === '/api/tree' && c.body.name === 'DF.B').length, 2);
const status = els['pysim-fs-probe-status'].textContent;
assert.match(status, /2\/2 files/);
assert.match(status, /0 present/);
assert.match(status, /2 absent/);
});
test('stop halts the walk and still reports a summary', async () => {
root([ef('EF.X', '6f01'), ef('EF.Y', '6f02')]);
setup([
{ path: '/api/select', name: 'EF.X', reply: () => { _pysimFsProbe.stop = true; return { exists: true }; } },
]);
await pysimFsProbeAll();
assert.deepStrictEqual(selectNames(), ['EF.X']);
const status = els['pysim-fs-probe-status'].textContent;
assert.ok(status.startsWith('Stopped —'), status);
assert.strictEqual(els['pysim-fs-probe-btn'].textContent, 'Probe all files');
});
+58
View File
@@ -0,0 +1,58 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name) {
const re = new RegExp('(?:async\\s+)?function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
}
let code = 'var pysimFsSort = "fid";\n';
code += extractFunc(html, 'pysimFsSortChildren') + '\n';
code += extractFunc(html, 'pysimFsRenderNode') + '\n';
code += 'globalThis.esc = s => s;\nglobalThis.t = s => s;\n';
eval(code);
const ef = (name, fid) => ({ name, fid, isDir: false, exists: true, children: null, expanded: false });
const df = (name, fid, extra) => Object.assign({ name, fid, isDir: true, exists: true, children: null, expanded: false }, extra || {});
test('absent directory renders a cross and no expand toggle', () => {
const node = df('DF.WLAN', '5f40', { exists: false, children: [] });
const out = pysimFsRenderNode(node, 0);
assert.ok(out.includes('✗'), out);
assert.ok(!out.includes('pysimFsToggleDir'), out);
assert.ok(!out.includes('▶'), out);
});
test('expanded empty directory shows the (empty) placeholder', () => {
const node = df('DF.EMPTY', '5f00', { children: [], expanded: true });
const out = pysimFsRenderNode(node, 0);
assert.ok(out.includes('(empty)'), out);
});
test('expanded directory with children renders no placeholder', () => {
const node = df('DF.GSM', '7f20', { children: [ef('EF.IMSI', '6f07')], expanded: true });
const out = pysimFsRenderNode(node, 0);
assert.ok(out.includes('EF.IMSI'), out);
assert.ok(!out.includes('(empty)'), out);
});
test('collapsed directory hides its children', () => {
const node = df('DF.GSM', '7f20', { children: [ef('EF.IMSI', '6f07')], expanded: false });
const out = pysimFsRenderNode(node, 0);
assert.ok(!out.includes('EF.IMSI'), out);
});
+7
View File
@@ -61,3 +61,10 @@ test('custom files form has add/save and cancel controls', () => {
assert.ok(html.includes("event.key==='Enter')pysimCustomSubmit()")); assert.ok(html.includes("event.key==='Enter')pysimCustomSubmit()"));
assert.ok(!html.includes('pysimCustomAdd')); assert.ok(!html.includes('pysimCustomAdd'));
}); });
test('file manager has a probe-all-files button and status line', () => {
assert.match(html, /id="pysim-fs-probe-btn"[^>]*data-needs="card"/);
assert.match(html, /id="pysim-fs-probe-btn"[^>]*data-l10n="Probe all files"/);
assert.ok(html.includes('onclick="pysimFsProbeAll()"'));
assert.ok(html.includes('id="pysim-fs-probe-status"'));
});
+1 -1
View File
@@ -2184,7 +2184,7 @@ class PysimHandler(BaseHTTPRequestHandler):
sys.stderr.write('Handler error: %s\n' % e) sys.stderr.write('Handler error: %s\n' % e)
if 'Card' in str(e) or 'Transaction' in str(e) or 'Transmit' in str(e): if 'Card' in str(e) or 'Transaction' in str(e) or 'Transmit' in str(e):
_handle_card_disconnect() _handle_card_disconnect()
err = {'success': False, 'error': str(e)} err = {'success': False, 'error': str(e), 'exists': False}
self._send_json(err, 500) self._send_json(err, 500)
self._log_resp(err) self._log_resp(err)
elif self.path == '/api/menu-select': elif self.path == '/api/menu-select':