scp81 passthru, proactive decoders, ADM badge, file manager layout (v2.2.1)

SCP81:
- New listener mode "passthru": no local listener - the card's BIP
  channels connect straight to a configured external platform
  (host/port required), which terminates TLS and runs the dialog.
  The status API reports mode/target (_SCP81_MODE/_SCP81_TARGET) and
  clears them on stop. PWA mode select, hint, Start validation; TLS PSK
  stays the default.

Proactive command decoding (Phone tab log):
- SEND SHORT MESSAGE (0x13) is now decoded: alpha, address (TON/NPI +
  number), 3GPP-SMS TPDU (type, TP-MR, TP-DA, TP-PID 0x7F flagged as
  SIM data download, TP-DCS, TP-VP, TP-UDL), UDH concatenation IEs, and
  the TP-UD as text (GSM-7 with a septet unpacker, UCS2, 8-bit) or as a
  TS 31.115 secured packet for PID 0x7F; malformed TPDUs fall back to
  the raw hex line.
- PROVIDE LOCAL INFORMATION qualifier names completed per TS 102 223
  V18.3.0: ESN (07), MEID (0B), Supported RATs (1A); 05 relabelled
  "Reserved for GSM (Timing Advance)". Fixed in the server dict and
  both frontend tables.

Header:
- Compact ADM badge next to the card indicator: "ADM ✓" green when
  pySim's adm_verified is set, "ADM ✗" red otherwise, hidden without a
  card session or when the server is down; updated ahead of the card
  state-key early return so it never disturbs the connect/reset flow.

File manager:
- Sort pills (FID/Name), Probe all files button and progress line are
  pinned above the tree instead of scrolling with it; the tree box cap
  grows from 420px to 65vh.

SW cache otaman-v165; help EN/RU updated; tests 234 Python + 361 frontend.
This commit is contained in:
2026-09-16 15:29:01 +03:00
parent 8c90958718
commit 20fbfd99a5
12 changed files with 479 additions and 39 deletions
+69 -17
View File
@@ -18,11 +18,12 @@
<div class="max-w-7xl mx-auto px-6 py-2">
<div class="flex items-center justify-between mb-3">
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v2.2.0</span></h1>
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v2.2.1</span></h1>
<div class="flex items-center gap-4">
<span id="state-indicator" class="flex items-center select-none" style="cursor:default" title="Connecting...">
<span id="state-indicator-dot" class="text-xs text-gray-400" title="Connecting..."></span>
<img id="state-indicator-img" class="hidden dark:invert" style="width:30px;height:30px" alt="" src="nosim.svg">
<span id="state-indicator-adm" class="hidden ml-0.5 text-xs font-mono" title=""></span>
</span>
<button id="install-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" style="display:none">INSTALL PWA [for offline use]</button>
<a href="https://github.com/anttro/otaman" target="_blank" class="text-xs text-gray-400 hover:text-gray-600 dark:text-slate-500 dark:hover:text-slate-300">github</a>
@@ -824,15 +825,19 @@
<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 class="flex items-center gap-1 mb-1 text-gray-500 dark:text-slate-400">
<span data-l10n="Sort:">Sort:</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>
<div class="w-1/3 flex flex-col">
<div class="shrink-0 text-xs font-mono">
<div class="flex items-center gap-1 mb-1 text-gray-500 dark:text-slate-400">
<span data-l10n="Sort:">Sort:</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>
</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>
<div class="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:65vh">
<div id="pysim-fs-tree"></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>
<div class="flex-1">
<div id="pysim-fs-detail" class="hidden">
@@ -939,6 +944,7 @@
<label class="block mb-1 text-xs font-medium text-gray-600 dark:text-slate-400" data-l10n="Mode">Mode</label>
<select id="scp81-mode" onchange="scp81ModeChanged()" class="border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800">
<option value="tls" data-l10n="PSK TLS server" selected>PSK TLS server</option>
<option value="passthru" data-l10n="Pass-through (external server)">Pass-through (external server)</option>
<option value="dump" data-l10n="Capture (dump)">Capture (dump)</option>
</select>
</div>
@@ -959,7 +965,8 @@
<button id="scp81-restart-btn" data-needs="server" onclick="scp81RestartScript()" class="px-3 py-1.5 text-sm rounded bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Restart script">Restart script</button>
</div>
<div id="scp81-msg" class="text-xs mt-2 hidden"></div>
<div class="mt-2 text-xs text-gray-500 dark:text-slate-400" data-l10n="PSK keys come from the card presets (Cards tab): the listener picks the key by the identity the card sends in the TLS handshake.">PSK keys come from the card presets (Cards tab): the listener picks the key by the identity the card sends in the TLS handshake.</div>
<div id="scp81-psk-note" class="mt-2 text-xs text-gray-500 dark:text-slate-400" data-l10n="PSK keys come from the card presets (Cards tab): the listener picks the key by the identity the card sends in the TLS handshake.">PSK keys come from the card presets (Cards tab): the listener picks the key by the identity the card sends in the TLS handshake.</div>
<div id="scp81-passthru-note" class="hidden mt-2 text-xs text-gray-500 dark:text-slate-400" data-l10n="Pass-through: every BIP channel the card opens is connected to this Host:Port (the external HTTP OTA platform); TLS is terminated there, the card's requested address is only logged.">Pass-through: every BIP channel the card opens is connected to this Host:Port (the external HTTP OTA platform); TLS is terminated there, the card's requested address is only logged.</div>
</div>
<div class="border border-gray-200 dark:border-slate-700 rounded p-3 mb-3">
<div class="flex justify-between items-center mb-2">
@@ -4585,6 +4592,7 @@ async function pysimFetch(path, body) {
let _pysimServerAvailable = null; // null until probed
let _pysimCardEquipped = false;
let _pysimEquipping = false;
let _pysimAdmVerified = null; // null = no card session
let _pysimAvailabilityTimer = null;
function pysimAvailabilityState() {
@@ -4638,9 +4646,33 @@ function pysimApplyAvailability() {
pysimUpdateStateIndicator();
}
// Compact ADM state next to the header's card indicator: "ADM ✓" when the
// administrator PIN was verified (pySim rs.adm_verified), "ADM ✗" otherwise.
// Hidden without a card session; only rewrites the DOM when the state changes
// (the 2s /api/status poll calls this on every update).
function pysimUpdateAdmIndicator(status) {
const el = document.getElementById('state-indicator-adm');
if (!el) return;
const verified = (status && status.connected) ? !!status.adm_verified : null;
if (verified === _pysimAdmVerified) return;
_pysimAdmVerified = verified;
el.classList.remove('text-emerald-600', 'dark:text-emerald-400', 'text-red-500');
if (verified === null) {
el.classList.add('hidden');
el.removeAttribute('title');
return;
}
el.textContent = verified ? 'ADM ✓' : 'ADM ✗';
el.classList.remove('hidden');
el.classList.add(verified ? 'text-emerald-600' : 'text-red-500');
if (verified) el.classList.add('dark:text-emerald-400');
el.setAttribute('title', t(verified ? 'ADM verified' : 'ADM not verified'));
}
function pysimSetServerAvailable(available) {
if (_pysimServerAvailable === available) return;
_pysimServerAvailable = available;
if (available !== true) pysimUpdateAdmIndicator(null);
pysimApplyAvailability();
}
@@ -6851,15 +6883,18 @@ const EVENT_FORMS = {
const PLI_QUALIFIERS = [
{code:'00',name:'Location Info (MCC, MNC, LAC/TAC, Cell ID)'},{code:'01',name:'IMEI'},
{code:'02',name:'Network Measurement results'},{code:'03',name:'Date, time and time zone'},
{code:'04',name:'Language setting'},{code:'05',name:'Timing Advance'},
{code:'06',name:'Access Technology (single)'},{code:'08',name:'IMEISV'},
{code:'04',name:'Language setting'},{code:'05',name:'Reserved for GSM (Timing Advance)'},
{code:'06',name:'Access Technology (single)'},{code:'07',name:'ESN of the terminal'},
{code:'08',name:'IMEISV'},
{code:'09',name:'Search Mode'},{code:'0A',name:'Battery charge state'},
{code:'0B',name:'MEID of the terminal'},
{code:'0C',name:'Current WSID'},{code:'0D',name:'Broadcast Network info'},
{code:'0E',name:'Multiple Access Technologies'},{code:'0F',name:'Location Info (multi-RAT)'},
{code:'10',name:'NMR (multi-RAT)'},{code:'11',name:'CSG ID list + HNB name'},
{code:'12',name:'H(e)NB IP address'},{code:'13',name:'H(e)NB surrounding macrocells'},
{code:'14',name:'Current WLAN identifier'},{code:'15',name:'Slices information'},
{code:'16',name:'CAG information list'},{code:'17',name:'Rejected slices information'},
{code:'1A',name:'Supported Radio Access Technologies'},
];
async function pysimEventsRender() {
@@ -7085,6 +7120,7 @@ function pysimCardStateUpdate(status) {
_pysimServerAvailable = true;
_pysimCardEquipped = !!status.connected;
_pysimEquipping = !!status.equipping;
pysimUpdateAdmIndicator(status);
pysimApplyAvailability();
if (pysimProactiveSeqChanged(status.proactive_seq)
&& isViewVisible('tab-phone') && isViewVisible('phone-sub-phone')) {
@@ -7180,11 +7216,12 @@ const CMD_QUALIFIER_SHORT = {
'23': {0x00:'Digits+Echo', 0x01:'Alpha+Echo', 0x04:'NoEcho'},
'24': {0x00:'Normal', 0x02:'Help', 0x04:'DefaultItem', 0x80:'Nav'},
'25': {0x00:'Normal', 0x04:'Help', 0x80:'Nav'},
'26': {0x00:'Loc', 0x01:'IMEI', 0x02:'NMR', 0x03:'Time', 0x04:'Lang', 0x05:'TA',
0x06:'AccTech', 0x08:'IMEISV', 0x09:'Search', 0x0A:'Batt', 0x0C:'WSID',
'26': {0x00:'Loc', 0x01:'IMEI', 0x02:'NMR', 0x03:'Time', 0x04:'Lang', 0x05:'TA (GSM)',
0x06:'AccTech', 0x07:'ESN', 0x08:'IMEISV', 0x09:'Search', 0x0A:'Batt',
0x0B:'MEID', 0x0C:'WSID',
0x0D:'BCInfo', 0x0E:'MultiAT', 0x0F:'MultiLoc', 0x10:'MultiNMR',
0x11:'CSG', 0x12:'HNB-IP', 0x13:'HNB-Macro', 0x14:'WLAN', 0x15:'Slices',
0x16:'CAG', 0x17:'RejSlice'},
0x16:'CAG', 0x17:'RejSlice', 0x1A:'SupRAT'},
'27': {0x00:'Start', 0x01:'Deactivate', 0x02:'Get'},
};
@@ -7293,6 +7330,10 @@ function scp81ModeChanged() {
const mode = document.getElementById('scp81-mode').value;
const row = document.getElementById('scp81-script-row');
if (row) row.style.display = (mode === 'tls') ? '' : 'none';
const pskNote = document.getElementById('scp81-psk-note');
if (pskNote) pskNote.classList.toggle('hidden', mode !== 'tls');
const directNote = document.getElementById('scp81-passthru-note');
if (directNote) directNote.classList.toggle('hidden', mode !== 'passthru');
}
function scp81SwitchSubtab(name) {
@@ -7666,10 +7707,16 @@ async function scp81LogRefresh() {
async function scp81Start() {
const mode = document.getElementById('scp81-mode').value;
const hostVal = document.getElementById('scp81-host').value.trim();
const portVal = document.getElementById('scp81-port').value.trim();
if (mode === 'passthru' && (!hostVal || !portVal)) {
scp81Msg(t('Pass-through requires the target host and port'), 'text-red-500');
return;
}
const body = {
action: 'start', mode: mode,
host: document.getElementById('scp81-host').value.trim() || '127.0.0.1',
port: parseInt(document.getElementById('scp81-port').value.trim() || '8443', 10),
host: hostVal || '127.0.0.1',
port: parseInt(portVal || '8443', 10),
};
if (mode === 'tls') {
const map = cardsPskMap();
@@ -10734,6 +10781,11 @@ const LANG_RU = {
'LOAD blocks': 'Блоки LOAD',
'clamped from': 'ограничено с',
'auto-fit': 'авто',
'Pass-through (external server)': 'Проброс (внешний сервер)',
'Pass-through: every BIP channel the card opens is connected to this Host:Port (the external HTTP OTA platform); TLS is terminated there, the card\'s requested address is only logged.': 'Проброс: каждый открываемый картой BIP-канал подключается к этому Host:Port (внешняя платформа HTTP OTA); TLS завершается там, запрошенный картой адрес только журналируется.',
'Pass-through requires the target host and port': 'Для режима проброса нужны host и port внешнего сервера',
'ADM verified': 'ADM подтверждён',
'ADM not verified': 'ADM не подтверждён',
};
let currentLang = 'en';