Compare commits

...

6 Commits

3 changed files with 889 additions and 18 deletions
+42
View File
@@ -396,6 +396,48 @@ Files not in pysim's model can be added manually:
Custom files persist in `localStorage` across sessions. Export/import as JSON for sharing.
### Proactive UICC Pill
The **Proactive UICC** sub-tab in the Card Reader provides real-time CAT session
interaction:
**Subscribed Events** — the card's SET UP EVENT LIST is displayed with per-event
**Send** buttons. Clicking opens a form specific to the event type:
- **No-data events** (User Activity, Idle Screen, etc.) — single-click confirmation
- **Location Status** — dropdown for Normal / Limited / No service
- **Access Technology Change** — dropdown for all 13 RAT types
- **Card Reader Status, Language, UICC Access** — appropriate inputs
- **Network Rejection** — full adaptive form with registration type dropdown
(LU / GPRS / EPS / 5GS), location fields (MCC, MNC, LAC, RAC, TAC), access
technology selection, and 53-cause unified rejection cause code dropdown
covering EMM, GMM, 5GMM, and LU causes
**Proactive Command Log** — chronological list of proactive commands encountered
(seconds elapsed, type code, name, byte count). Covers SET UP MENU, SET UP EVENT
LIST, POLL INTERVAL, DISPLAY TEXT, SELECT ITEM, and PROVIDE LOCAL INFORMATION.
**PLI Data Dictionary** — editable per-qualifier hex values for all 22 PROVIDE
LOCAL INFORMATION qualifiers (TS 102 223 + TS 131 111). 10 qualifiers have
inline decode/encode forms (▶ toggle):
| Code | Decoded fields |
|------|--------------|
| 00 | MCC, MNC, LAC/TAC |
| 01 | IMEI (15 digits) |
| 03 | Date, Time, TZ offset |
| 04 | Language (2-char code) |
| 05 | ME Status, Timing Advance |
| 06 | Access Technology (dropdown) |
| 08 | IMEISV (16 digits) |
| 09 | Search Mode (Auto/Manual) |
| 0A | Battery charge (%) |
| 0E | Multiple Access Technologies (comma-list) |
Values persist on the server until restart. Apply → hex updates; Save → POSTs to
server. The server will use these values to populate TERMINAL RESPONSE data for
future PLI proactive commands.
### Command Hints
Type a command name in the **pySim command line** input. Usage hints appear as a tooltip after 300ms. Command autocomplete suggestions appear above the input.
+726 -18
View File
@@ -16,7 +16,7 @@
<div class="max-w-7xl mx-auto px-6 py-4">
<div class="flex items-center justify-between mb-6">
<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">v1.4.3</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">v1.5.0</span></h1>
<div class="flex items-center gap-4">
<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>
@@ -926,11 +926,38 @@
<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-proactive" class="hidden">
<div class="mb-2 text-xs text-gray-500 dark:text-slate-400" data-l10n="Events that the card monitors (SET UP EVENT LIST):">Events that the card monitors (SET UP EVENT LIST):</div>
<div id="pysim-event-list" class="text-xs font-mono text-gray-600 dark:text-slate-400"></div>
<div class="mt-3 mb-2 text-xs text-gray-500 dark:text-slate-400" data-l10n="Fetched proactive commands:">Fetched proactive commands:</div>
<div id="pysim-proactive-log" class="text-xs font-mono text-gray-600 dark:text-slate-400"></div>
<div class="flex gap-4">
<div>
<button id="pli-status-btn" onclick="pysimStatusPoll()" class="mb-1 px-2 py-0.5 text-[10px] rounded bg-gray-600 text-white hover:bg-gray-700" data-l10n="Send STATUS">Send STATUS</button>
<div class="mb-2 text-xs text-gray-500 dark:text-slate-400" data-l10n="Events that the card monitors (SET UP EVENT LIST):">Events that the card monitors (SET UP EVENT LIST):</div>
<div id="pysim-event-list" class="text-xs font-mono text-gray-600 dark:text-slate-400"></div>
<div class="mt-3 mb-2 text-xs text-gray-500 dark:text-slate-400" data-l10n="Fetched proactive commands:">Fetched proactive commands:</div>
<div id="pysim-proactive-log" class="text-xs font-mono text-gray-600 dark:text-slate-400"></div>
</div>
<div>
<div class="flex justify-between items-center mb-2">
<span class="text-xs text-gray-500 dark:text-slate-400" data-l10n="PROVIDE LOCAL INFORMATION response data">PROVIDE LOCAL INFORMATION response data</span>
<button id="pli-save-btn" onclick="pysimPliSave()" class="px-2 py-0.5 text-xs rounded bg-emerald-600 text-white hover:bg-emerald-700" data-l10n="Save">Save</button>
</div>
<div id="pysim-pli-dict" class="text-xs font-mono text-gray-600 dark:text-slate-400 max-h-[60vh] overflow-auto">
<span class="text-gray-400" data-l10n="Loading...">Loading...</span>
</div>
</div>
</div>
</div>
<div id="event-send-modal" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center hidden">
<div class="bg-white dark:bg-slate-800 rounded-lg p-6 max-w-sm w-full mx-4 max-h-[85vh] overflow-auto">
<h3 id="event-send-title" class="text-lg font-semibold mb-4 text-gray-800 dark:text-slate-200"></h3>
<div id="event-send-body" class="space-y-3"></div>
<div id="event-send-error" class="text-xs text-red-500 hidden mt-2"></div>
<div id="event-send-result" class="text-xs text-emerald-600 dark:text-emerald-400 hidden mt-2"></div>
<div class="flex gap-2 mt-4 justify-end">
<button onclick="document.getElementById('event-send-modal').classList.add('hidden')" class="px-3 py-1.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-100 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300" data-l10n="Cancel">Cancel</button>
<button id="event-send-btn" onclick="pysimEventSend()" class="px-4 py-1.5 text-xs rounded bg-blue-600 text-white hover:bg-blue-700" data-l10n="Send">Send</button>
</div>
</div>
</div>
</div>
<div id="stk-menu-panel" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center hidden">
@@ -3017,10 +3044,10 @@ function cardsExport() {
el.select();
}
function cardsImport() {
const el = document.getElementById('cards-io');
async function cardsImport() {
try {
const imported = JSON.parse(el.value);
const text = await navigator.clipboard.readText();
const imported = JSON.parse(text);
if (!Array.isArray(imported)) throw new Error('Expected an array');
let added = 0;
for (const entry of imported) {
@@ -3042,6 +3069,7 @@ function cardsImport() {
}
cardsSave();
cardsRender();
const el = document.getElementById('cards-io');
el.value = 'Imported ' + added + ' cards.';
setTimeout(() => { el.classList.add('hidden'); }, 2000);
} catch (e) {
@@ -3067,7 +3095,7 @@ function pysimSwitchSubtab(name) {
document.getElementById('pysim-sub-' + s).classList.toggle('hidden', s !== name);
});
if (name === 'custom') pysimCustomRender();
if (name === 'proactive') { pysimEventsRender(); pysimProactiveLogRender(); }
if (name === 'proactive') { pysimEventsRender(); pysimProactiveLogRender(); pysimPliRender(); }
}
function pysimCmdKeydown(e) {
@@ -3555,6 +3583,165 @@ const EVENT_NAMES = {
0x20: 'EARFCN Update', 0x21: 'SCEF Channel Status',
};
const REJECTION_CAUSES = [
{v:2,l:'02 \u2014 IMSI unknown in HLR/HSS'},{v:3,l:'03 \u2014 Illegal UE / MS'},
{v:5,l:'05 \u2014 IMEI / PEI not accepted'},{v:6,l:'06 \u2014 Illegal ME'},
{v:7,l:'07 \u2014 Services not allowed'},{v:8,l:'08 \u2014 Services + non-services not allowed'},
{v:9,l:'09 \u2014 UE identity cannot be derived'},{v:10,l:'0A \u2014 Implicitly detached / de-registered'},
{v:11,l:'0B \u2014 PLMN not allowed'},{v:12,l:'0C \u2014 Location / Tracking Area not allowed'},
{v:13,l:'0D \u2014 Roaming not allowed in this area'},{v:14,l:'0E \u2014 Services not allowed in this PLMN'},
{v:15,l:'0F \u2014 No Suitable Cells'},{v:16,l:'10 \u2014 MSC temporarily not reachable'},
{v:17,l:'11 \u2014 Network failure'},
{v:18,l:'12 \u2014 CS domain not avail. (EMM) / MAC fail (GMM)'},
{v:19,l:'13 \u2014 ESM failure (EMM) / Synch failure (GMM)'},
{v:20,l:'14 \u2014 MAC failure (EMM) / Congestion (GMM)'},
{v:21,l:'15 \u2014 Synch failure (EMM)'},{v:22,l:'16 \u2014 Congestion / PLMN rejected perm. (LU)'},
{v:23,l:'17 \u2014 UE security capabilities mismatch'},{v:24,l:'18 \u2014 Security mode rejected, unspecified'},
{v:25,l:'19 \u2014 Not authorized for this CSG'},{v:26,l:'1A \u2014 Non-3GPP/5G auth unacceptable'},
{v:27,l:'1B \u2014 N1 mode not allowed (5GMM)'},{v:28,l:'1C \u2014 Restricted service area (5GMM)'},
{v:31,l:'1F \u2014 Redirection to EPC required (5GMM)'},
{v:32,l:'20 \u2014 Service option not supported (LU)'},
{v:33,l:'21 \u2014 Requested service option not subscribed (LU)'},
{v:34,l:'22 \u2014 Service option temporarily out of order (LU)'},
{v:35,l:'23 \u2014 Requested service option not authorized (EMM)'},
{v:38,l:'26 \u2014 Call cannot be identified (LU)'},
{v:39,l:'27 \u2014 CS service temporarily not available (EMM)'},
{v:40,l:'28 \u2014 No EPS bearer context activated (EMM)'},
{v:42,l:'2A \u2014 Severe network failure (EMM)'},{v:43,l:'2B \u2014 LADN not available (5GMM)'},
{v:48,l:'30 \u2014 Retry upon entry into new cell (LU)'},
{v:62,l:'3E \u2014 No network slices available (5GMM)'},
{v:65,l:'41 \u2014 Maximum PDU sessions reached (5GMM)'},
{v:67,l:'43 \u2014 Insufficient resources for slice/DNN (5GMM)'},
{v:69,l:'45 \u2014 Insufficient resources for slice (5GMM)'},
{v:71,l:'47 \u2014 ngKSI already in use (5GMM)'},
{v:72,l:'48 \u2014 Non-3GPP access to 5GCN not allowed (5GMM)'},
{v:73,l:'49 \u2014 Serving network not authorized (5GMM)'},
{v:74,l:'4A \u2014 Temporarily not authorized for SNPN (5GMM)'},
{v:75,l:'4B \u2014 Permanently not authorized for SNPN (5GMM)'},
{v:76,l:'4C \u2014 Not authorized for this CAG (5GMM)'},
{v:90,l:'5A \u2014 Payload was not forwarded (5GMM)'},
{v:91,l:'5B \u2014 DNN not supported/subscribed in slice (5GMM)'},
{v:92,l:'5C \u2014 Insufficient UP resources for PDU session (5GMM)'},
{v:95,l:'5F \u2014 Semantically incorrect message'},{v:96,l:'60 \u2014 Invalid mandatory information'},
{v:97,l:'61 \u2014 Message type non-existent/not implemented'},
{v:98,l:'62 \u2014 Message type not compatible with protocol state'},
{v:99,l:'63 \u2014 IE non-existent/not implemented'},
{v:100,l:'64 \u2014 Conditional IE error'},{v:101,l:'65 \u2014 Message not compatible with protocol state'},
{v:111,l:'6F \u2014 Protocol error, unspecified'},
];
const EVENT_FORMS = {
0x00: { title: 'MT call', note: 'not_yet' },
0x01: { title: 'Call connected', note: 'not_yet' },
0x02: { title: 'Call disconnected', note: 'not_yet' },
0x03: { title: 'Location status',
fields: [
{ id: 'status', label: 'Service Status', type: 'select',
opts: [{v:0,l:'Normal service'},{v:1,l:'Limited service'},{v:2,l:'No service'}] }
],
build(v) { return '9B01' + (v.status|0).toString(16).padStart(2,'0'); }
},
0x04: null,
0x05: null,
0x06: { title: 'Card reader status',
fields: [
{ id: 'status', label: 'Status', type: 'select',
opts: [{v:0,l:'Card reader removed'},{v:1,l:'Card reader inserted'}] }
],
build(v) { return '' + (v.status|0).toString(16).padStart(2,'0'); }
},
0x07: { title: 'Language selection',
fields: [
{ id: 'lang', label: 'Language code', type: 'text', placeholder: 'ISO 639, e.g. en, ru', pattern: '[a-zA-Z]{2,3}' }
],
build(v) { const b = new TextEncoder().encode(v.lang||'en'); return b.length.toString(16).padStart(2,'0') + bytesToHex(Array.from(b)); }
},
0x08: { title: 'Browser termination', note: 'not_yet' },
0x09: null,
0x0A: { title: 'Channel status', note: 'not_yet' },
0x0B: { title: 'Access Technology Change',
fields: [
{ id: 'tech', label: 'Access Technology', type: 'select',
opts: [{v:0,l:'GSM'},{v:1,l:'GSM Compact'},{v:3,l:'UTRAN'},{v:4,l:'GSM w/EGPRS'},{v:5,l:'UTRAN w/HSDPA'},{v:6,l:'UTRAN w/HSUPA'},{v:7,l:'UTRAN w/HSPA+'},{v:8,l:'E-UTRAN'},{v:9,l:'E-UTRAN (4G)'},{v:10,l:'5G NR'},{v:11,l:'5G NR (NSA)'},{v:12,l:'Satellite'}] }
],
build(v) { return 'BF01' + (v.tech|0).toString(16).padStart(2,'0'); }
},
0x0C: null,
0x0D: null,
0x0E: null,
0x0F: null,
0x10: null,
0x11: { title: 'I-WLAN Access Status', note: 'not_yet' },
0x12: { title: 'Network Rejection',
fields: [
{ id: 'reg_type', label: 'Registration type', type: 'select', onchange: 'updateNetRejectionFields()',
opts: [
{v:0x00,l:'Normal Location Updating',g:'lu'},{v:0x01,l:'Periodic Location Updating',g:'lu'},
{v:0x02,l:'IMSI Attach',g:'lu'},{v:0x03,l:'GPRS Attach',g:'gprs'},
{v:0x04,l:'Combined GPRS/IMSI Attach',g:'gprs'},{v:0x05,l:'RA Updating',g:'gprs'},
{v:0x06,l:'Combined RA/LA Updating',g:'gprs'},{v:0x07,l:'Combined RA/LA Updating w/ IMSI Attach',g:'gprs'},
{v:0x08,l:'Periodic RA Updating',g:'gprs'},{v:0x09,l:'EPS Attach (EMM)',g:'eps'},
{v:0x0A,l:'Combined EPS/IMSI Attach (EMM)',g:'eps'},{v:0x0B,l:'TA Updating (TAU)',g:'eps'},
{v:0x0C,l:'Combined TA/LA Updating (TAU)',g:'eps'},{v:0x0D,l:'Combined TA/LA Updating w/ IMSI Attach (TAU)',g:'eps'},
{v:0x0E,l:'Periodic TA Updating (TAU)',g:'eps'},{v:0x0F,l:'Initial Registration (5GS)',g:'eps'},
{v:0x10,l:'Mobility Registration Updating (5GS)',g:'eps'},{v:0x11,l:'Periodic Registration Updating (5GS)',g:'eps'},
]},
{ id: 'mcc', label: 'MCC', type: 'text', pattern: '\\d{3}', placeholder: 'e.g. 250', g: 'lu,gprs,eps' },
{ id: 'mnc', label: 'MNC', type: 'text', pattern: '\\d{2,3}', placeholder: 'e.g. 01', g: 'lu,gprs,eps' },
{ id: 'lac', label: 'LAC', type: 'text', pattern: '[0-9A-Fa-f]{4}', placeholder: 'hex, e.g. 00FF', g: 'lu,gprs' },
{ id: 'rac', label: 'RAC', type: 'text', pattern: '[0-9A-Fa-f]{2}', placeholder: 'hex, e.g. 01', g: 'gprs' },
{ id: 'tac', label: 'TAC', type: 'text', pattern: '[0-9A-Fa-f]{4,6}', placeholder: '4 (E-UTRAN) or 6 (NR) hex chars', g: 'eps' },
{ id: 'access_tech', label: 'Access Technology', type: 'select',
opts: [{v:0x00,l:'GSM'},{v:0x03,l:'UTRAN'},{v:0x08,l:'E-UTRAN'},{v:0x0A,l:'NG-RAN'},{v:0x0B,l:'Satellite NG-RAN'},{v:0x0C,l:'Satellite E-UTRAN'}], g: 'lu,gprs,eps' },
{ id: 'cause', label: 'Rejection cause code', type: 'select', opts: REJECTION_CAUSES, g: 'lu,gprs,eps' },
{ id: 'ext_cause', label: 'Extended EMM cause (optional)', type: 'number', min: '0', max: '255', placeholder: '0255', g: 'eps' },
],
build(v) {
const rt = parseInt(v.reg_type) || 0;
const g = rt <= 0x02 ? 'lu' : rt <= 0x08 ? 'gprs' : 'eps';
let d = 'F401' + rt.toString(16).padStart(2,'0');
d += 'BF01' + (parseInt(v.access_tech)|0).toString(16).padStart(2,'0');
d += 'F501' + (parseInt(v.cause) || 2).toString(16).padStart(2,'0');
const pm = encPlmn(v.mcc||'250',v.mnc||'01');
if (g === 'lu') {
d += '9305' + pm + ((v.lac||'0000').toUpperCase());
} else if (g === 'gprs') {
d += 'F306' + pm + ((v.lac||'0000').toUpperCase()) + ((v.rac||'01').toUpperCase());
} else {
const tc = (v.tac||'0001').toUpperCase();
d += '7D0' + (3 + tc.length/2).toString(16) + pm + tc;
if (v.ext_cause !== '' && v.ext_cause !== undefined) {
d += '5701' + (parseInt(v.ext_cause)|0).toString(16).padStart(2,'0');
}
}
return d.toUpperCase();
}
},
0x13: null,
0x14: { title: 'Change of UICC Access',
fields: [{ id: 'tech', label: 'Access Technology', type: 'select',
opts: [{v:0,l:'GSM'},{v:3,l:'UTRAN'},{v:8,l:'E-UTRAN'},{v:10,l:'NG-RAN'}] }],
build(v) { return 'BF01' + (v.tech|0).toString(16).padStart(2,'0'); }
},
0x15: { title: 'CSG Cell Change', note: 'not_yet' },
0x16: null,
0x17: null,
};
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:'09',name:'Search Mode'},{code:'0A',name:'Battery charge state'},
{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'},
];
async function pysimEventsRender() {
const el = document.getElementById('pysim-event-list');
el.innerHTML = '<span class="text-gray-400">' + t('Loading...') + '</span>';
@@ -3567,7 +3754,10 @@ async function pysimEventsRender() {
let html = '';
events.forEach(e => {
const name = EVENT_NAMES[e] || ('Event 0x' + e.toString(16).padStart(2, '0').toUpperCase());
html += '<div class="flex items-center gap-2 py-0.5"><span class="w-6 text-right text-gray-400">' + e.toString(16).padStart(2, '0').toUpperCase() + '</span> ' + esc(name) + '</div>';
html += '<div class="flex items-center gap-2 py-0.5"><span class="w-6 text-right text-gray-400">'
+ e.toString(16).padStart(2, '0').toUpperCase()
+ '</span> ' + esc(name)
+ '<button onclick="pysimEventSendForm(' + e + ')" class="ml-auto px-1.5 py-0.5 text-[10px] rounded bg-blue-600 text-white hover:bg-blue-700">' + t('Send') + '</button></div>';
});
el.innerHTML = html;
} catch (e) {
@@ -3575,6 +3765,150 @@ async function pysimEventsRender() {
}
}
let _eventFormType = null;
function pysimEventSendForm(eventType) {
const modal = document.getElementById('event-send-modal');
const title = document.getElementById('event-send-title');
const body = document.getElementById('event-send-body');
const err = document.getElementById('event-send-error');
const result = document.getElementById('event-send-result');
const btn = document.getElementById('event-send-btn');
err.classList.add('hidden');
result.classList.add('hidden');
err.textContent = '';
result.textContent = '';
const name = EVENT_NAMES[eventType] || ('0x' + eventType.toString(16).padStart(2,'0'));
const cfg = EVENT_FORMS[eventType];
_eventFormType = eventType;
if (!cfg) {
title.textContent = name + ' (' + t('Send') + ')';
body.innerHTML = '<p class="text-sm text-gray-600 dark:text-slate-400">' + t('Send') + ' <span class="font-mono">' + esc(name) + '</span> ' + t('event notification to the card?') + '</p>';
btn.textContent = t('Send');
modal.classList.remove('hidden');
return;
}
if (cfg.note === 'not_yet') {
title.textContent = name;
body.innerHTML = '<p class="text-sm text-gray-500 dark:text-slate-400">' + t('This event type is not yet supported.') + '</p>';
btn.classList.add('hidden');
} else {
title.textContent = name + ' (' + t('Send') + ')';
let html = '';
cfg.fields.forEach(f => {
const rg = f.g ? f.g.split(',') : null;
html += '<div class="event-field" data-groups="' + (f.g||'') + '" data-id="' + f.id + '">';
html += '<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300">' + t(f.label) + '</label>';
if (f.type === 'select') {
html += '<select id="ev-f-' + f.id + '"' + (f.onchange ? ' onchange="' + f.onchange + '"' : '') + ' class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800">';
f.opts.forEach(o => html += '<option value="' + o.v + '"' + (o.g ? ' data-gr="' + o.g + '"' : '') + '>' + esc(o.l) + '</option>');
html += '</select>';
} else {
const attrs = [];
if (f.pattern) attrs.push('pattern="' + f.pattern + '"');
if (f.placeholder) attrs.push('placeholder="' + f.placeholder + '"');
if (f.min !== undefined) attrs.push('min="' + f.min + '" max="' + f.max + '"');
html += '<input id="ev-f-' + f.id + '" type="' + f.type + '" ' + attrs.join(' ') + ' class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono">';
}
html += '</div>';
});
body.innerHTML = html;
btn.classList.remove('hidden');
if (eventType === 0x12) updateNetRejectionFields();
}
modal.classList.remove('hidden');
}
function updateNetRejectionFields() {
const sel = document.getElementById('ev-f-reg_type');
if (!sel) return;
const opt = sel.options[sel.selectedIndex];
const g = opt ? opt.getAttribute('data-gr') || 'lu' : 'lu';
document.querySelectorAll('#event-send-body .event-field').forEach(div => {
const groups = div.getAttribute('data-groups') || '';
const show = groups === '' || groups.indexOf(g) >= 0;
div.style.display = show ? '' : 'none';
});
}
async function pysimEventSend() {
const err = document.getElementById('event-send-error');
const result = document.getElementById('event-send-result');
const btn = document.getElementById('event-send-btn');
err.classList.add('hidden');
result.classList.add('hidden');
const cfg = EVENT_FORMS[_eventFormType];
let eventData = null;
if (cfg && cfg.build) {
const values = {};
let valid = true;
cfg.fields.forEach(f => {
const el = document.getElementById('ev-f-' + f.id);
if (!el || el.closest('.event-field').style.display === 'none') return;
const v = el.value.trim();
values[f.id] = v;
if (f.pattern && v && !new RegExp('^' + f.pattern + '$').test(v)) {
err.textContent = t('Invalid') + ': ' + t(f.label);
err.classList.remove('hidden');
valid = false;
}
});
if (!valid) return;
try {
eventData = cfg.build(values);
} catch (e) {
err.textContent = 'Error: ' + e.message;
err.classList.remove('hidden');
return;
}
}
btn.disabled = true;
btn.textContent = t('Sending...');
try {
const resp = await pysimFetch('/api/event-send', { event_type: _eventFormType, event_data: eventData });
if (resp.sw === '9000') {
result.textContent = (eventData ? t('Event data sent.') : t('Event sent.')) + ' SW: ' + resp.sw;
result.classList.remove('hidden');
} else {
result.textContent = 'SW: ' + resp.sw;
result.classList.remove('hidden');
result.classList.add('text-amber-600');
}
} catch (e2) {
err.textContent = 'Error: ' + e2.message;
err.classList.remove('hidden');
} finally {
btn.disabled = false;
btn.textContent = t('Send');
}
}
async function pysimStatusPoll() {
const btn = document.getElementById('pli-status-btn');
if (!btn) return;
btn.disabled = true;
btn.textContent = t('STATUS...');
try {
const resp = await pysimFetch('/api/status-poll', {});
if (resp.fetch) {
btn.textContent = 'SW: ' + resp.sw + ' + ' + resp.fetch_sw;
} else {
btn.textContent = 'SW: ' + resp.sw;
}
pysimEventsRender();
pysimProactiveLogRender();
} catch (e) {
btn.textContent = t('Error');
} finally {
setTimeout(() => { btn.textContent = t('Send STATUS'); btn.disabled = false; }, 2000);
}
}
// ===== proactive command log =====
const CMD_NAMES = {
'03': 'POLL INTERVAL', '05': 'SET UP EVENT LIST',
@@ -3585,6 +3919,14 @@ const CMD_NAMES = {
'15': 'LAUNCH BROWSER', '70': 'ACTIVATE',
};
const CMD_QUALIFIER_SHORT = {
'26': {0x00:'Loc', 0x01:'IMEI', 0x02:'NMR', 0x03:'Time', 0x04:'Lang', 0x05:'TA',
0x06:'AccTech', 0x08:'IMEISV', 0x09:'Search', 0x0A:'Batt', 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'},
};
async function pysimProactiveLogRender() {
const el = document.getElementById('pysim-proactive-log');
el.innerHTML = '<span class="text-gray-400">' + t('Loading...') + '</span>';
@@ -3595,12 +3937,23 @@ async function pysimProactiveLogRender() {
return;
}
let html = '';
log.forEach(e => {
log.forEach((e, i) => {
const name = CMD_NAMES[e.type_hex] || ('Cmd 0x' + e.type_hex.toUpperCase());
html += '<div class="flex items-center gap-2 py-0.5">'
+ '<span class="w-10 text-right text-gray-400">0x' + e.type_hex.toUpperCase() + '</span>'
+ '<span class="w-40 truncate">' + esc(name) + '</span>'
+ '<span class="text-gray-400">— ' + e.elapsed + 's (' + e.bytes + ' bytes)</span>'
let qual = '';
if (e.qualifier) {
const qs = CMD_QUALIFIER_SHORT[e.type_hex];
if (qs) qual = ' [' + (qs[parseInt(e.qualifier,16)] || '0x'+e.qualifier.toUpperCase()) + ']';
else qual = ' [0x' + e.qualifier.toUpperCase() + ']';
}
html += '<div data-plog-row="' + i + '">'
+ '<div class="flex items-center gap-2 py-0.5">'
+ (e.tr_hex ? '<span class="cursor-pointer select-none text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" onclick="pysimPlogToggle(' + i + ')" id="plog-tog-' + i + '">▶</span>' : '<span class="w-3"></span>')
+ '<span class="w-12 text-right text-gray-400">' + e.elapsed + 's</span>'
+ '<span class="font-mono text-gray-400">0x' + e.type_hex.toUpperCase() + '</span>'
+ '<span class="truncate">' + esc(name) + '</span>'
+ (qual ? '<span class="text-[10px] text-gray-500 dark:text-slate-400">' + esc(qual) + '</span>' : '')
+ '</div>'
+ (e.tr_hex ? '<div class="pli-decode-panel hidden ml-4 pl-2 border-l border-gray-300 dark:border-slate-600" data-plog-data="' + i + '"><div class="text-[10px] font-mono text-gray-500 dark:text-slate-400 max-h-20 overflow-auto whitespace-pre-wrap">Response: ' + esc(e.tr_hex) + '\nSW: ' + esc(e.tr_sw) + '</div></div>' : '')
+ '</div>';
});
el.innerHTML = html;
@@ -3609,6 +3962,336 @@ async function pysimProactiveLogRender() {
}
}
function pysimPlogToggle(idx) {
const panel = document.querySelector('[data-plog-data="' + idx + '"]');
const tog = document.getElementById('plog-tog-' + idx);
if (!panel || !tog) return;
if (panel.classList.contains('hidden')) {
panel.classList.remove('hidden');
tog.textContent = '▼';
} else {
panel.classList.add('hidden');
tog.textContent = '▶';
}
}
// ===== PLI data dictionary =====
function decPlmn(hex3) {
const h = hex3.replace(/\s/g, '').slice(0, 6);
if (h.length < 6) return ['250', '01'];
const b1 = parseInt(h[0]+h[1], 16), b2 = parseInt(h[2]+h[3], 16), b3 = parseInt(h[4]+h[5], 16);
const mcc = '' + (b1&0xF) + ((b1>>4)&0xF) + (b2&0xF);
let mnc = '' + (b3&0xF);
if ((b2>>4) !== 0xF) mnc += ((b2>>4)&0xF);
return [mcc, mnc];
}
function decImei(hex8) {
const h = hex8.replace(/\s/g, '').slice(0, 16);
if (h.length < 16) return '';
let s = '';
for (let i = 0; i < 15; i += 2) {
const b = parseInt(h[i]+h[i+1], 16);
s += '' + (b&0xF) + ((b>>4)&0xF);
}
return s.slice(0, 15);
}
function encImeiPli(digits) {
const d = digits.replace(/\D/g, '').slice(0, 15);
if (d.length < 15) return '';
let h = '';
for (let i = 0; i < 15; i += 2)
h += (parseInt(d[i+1])<<4 | parseInt(d[i])).toString(16).padStart(2, '0');
return h;
}
function encImeisvPli(digits) {
const d = digits.replace(/\D/g, '').slice(0, 16);
if (d.length < 16) return '';
let h = '';
for (let i = 0; i < 16; i += 2)
h += (parseInt(d[i+1])<<4 | parseInt(d[i])).toString(16).padStart(2, '0');
return h;
}
const ACCESSTECH = [
{v:0,l:'GSM'},{v:1,l:'GSM Compact'},{v:2,l:'TIA/EIA-533'},{v:3,l:'UTRAN'},
{v:4,l:'TETRA'},{v:5,l:'TIA/EIA-95-B'},{v:6,l:'CDMA2000 1x'},{v:7,l:'CDMA2000 HRPD'},
{v:8,l:'E-UTRAN'},{v:9,l:'eHRPD'},{v:10,l:'NG-RAN'},{v:11,l:'Satellite NG-RAN'},{v:12,l:'Satellite E-UTRAN'},
];
const PLI_CODECS = {
'00': { tlv: '9305',
decode(hex) {
const v = hex.replace(/\s/g, '').slice(4); if (v.length < 10) return null;
const pl = decPlmn(v); return { mcc: pl[0], mnc: pl[1], lac: v.slice(6,10).toUpperCase() };
},
encode(v) { return '9305' + encPlmn(v.mcc||'250',v.mnc||'01') + ((v.lac||'0000').toUpperCase()); },
fields: [
{id:'mcc',label:'MCC',type:'text',pattern:'\\d{3}',placeholder:'e.g. 250'},
{id:'mnc',label:'MNC',type:'text',pattern:'\\d{2,3}',placeholder:'e.g. 01'},
{id:'lac',label:'LAC/TAC',type:'text',pattern:'[0-9A-Fa-f]{4}',placeholder:'hex'},
],
},
'01': { tlv: '9408',
decode(hex) {
const v = hex.replace(/\s/g, '').slice(4); if (v.length < 16) return null;
return { imei: decImei(v) };
},
encode(v) { const imei = encImeiPli(v.imei||''); return imei ? '9408'+imei : ''; },
fields: [
{id:'imei',label:'IMEI',type:'text',pattern:'\\d{15}',placeholder:'15 digits'},
],
},
'03': { tlv: '2607',
decode(hex) {
const v = hex.replace(/\s/g, '').slice(4); if (v.length < 14) return null;
const yr = parseInt(v[0]+v[1])<70 ? 2000+parseInt(v[0]+v[1]) : 1900+parseInt(v[0]+v[1]);
const mo = parseInt(v[2]+v[3]), dy = parseInt(v[4]+v[5]);
const hh = parseInt(v[6]+v[7]), mm = parseInt(v[8]+v[9]), ss = parseInt(v[10]+v[11]);
const tz = parseInt(v[12]+v[13], 16);
const tzSign = (tz & 0x80) ? '-' : '+';
const tzH = Math.floor(((tz & 0x3F) || 0) / 4);
const tzM = (((tz & 0x3F) || 0) % 4) * 15;
return {
dt: yr+'-'+(''+mo).padStart(2,'0')+'-'+(''+dy).padStart(2,'0'),
tm: (''+hh).padStart(2,'0')+':'+(''+mm).padStart(2,'0')+':'+(''+ss).padStart(2,'0'),
tz: tzSign+(''+tzH).padStart(2,'0')+':'+(''+tzM).padStart(2,'0'),
};
},
encode(v) {
const dt = (v.dt||'2000-01-01').split('-'), tm = (v.tm||'00:00:00').split(':');
let tzQ = 0;
const tzM = v.tz ? v.tz.match(/^([-+])(\d{2}):?(\d{2})$/) : null;
if (tzM) { const h = parseInt(tzM[2])||0, m = parseInt(tzM[3])||0; tzQ = h*4 + Math.floor(m/15); if (tzM[1]==='-') tzQ |= 0x80; }
return '2607'
+ (parseInt(dt[0])%100).toString(16).padStart(2,'0')
+ (parseInt(dt[1])||1).toString(16).padStart(2,'0')
+ (parseInt(dt[2])||1).toString(16).padStart(2,'0')
+ (parseInt(tm[0])||0).toString(16).padStart(2,'0')
+ (parseInt(tm[1])||0).toString(16).padStart(2,'0')
+ (parseInt(tm[2])||0).toString(16).padStart(2,'0')
+ tzQ.toString(16).padStart(2,'0');
},
fields: [
{id:'dt',label:'Date',type:'date'},{id:'tm',label:'Time',type:'time'},
{id:'tz',label:'TZ offset',type:'text',pattern:'[-+]\\d{2}:?\\d{2}',placeholder:'+00:00'},
],
},
'04': { tlv: 'AD02',
decode(hex) {
const v = hex.replace(/\s/g, '').slice(4); if (v.length < 4) return null;
return { lang: String.fromCharCode(parseInt(v[0]+v[1],16), parseInt(v[2]+v[3],16)) };
},
encode(v) {
const lang = (v.lang||'en').slice(0,2);
return 'AD02' + lang.charCodeAt(0).toString(16).padStart(2,'0') + lang.charCodeAt(1).toString(16).padStart(2,'0');
},
fields: [
{id:'lang',label:'Language',type:'text',pattern:'[a-zA-Z]{2,3}',placeholder:'en'},
],
},
'05': { tlv: 'C602',
decode(hex) {
const v = hex.replace(/\s/g, '').slice(4); if (v.length < 4) return null;
return { status: parseInt(v[0]+v[1],16).toString(), ta: parseInt(v[2]+v[3],16).toString() };
},
encode(v) { return 'C602' + (parseInt(v.status)||0).toString(16).padStart(2,'0') + (parseInt(v.ta)||0).toString(16).padStart(2,'0'); },
fields: [
{id:'status',label:'ME Status',type:'number',min:'0',max:'255',placeholder:'0'},
{id:'ta',label:'Timing Advance',type:'number',min:'0',max:'255',placeholder:'0'},
],
},
'06': { tlv: 'BF01',
decode(hex) {
const v = hex.replace(/\s/g, '').slice(4); if (v.length < 2) return null;
return { tech: parseInt(v[0]+v[1],16).toString() };
},
encode(v) { return 'BF01' + (parseInt(v.tech)||0).toString(16).padStart(2,'0'); },
fields: [{id:'tech',label:'Access Technology',type:'select',opts:ACCESSTECH}],
},
'08': { tlv: '9408',
decode(hex) {
const v = hex.replace(/\s/g, '').slice(4); if (v.length < 16) return null;
return { imeisv: decImei(v) + v.slice(14,16).toUpperCase() };
},
encode(v) {
const raw = (v.imeisv||'').replace(/\D/g, '').slice(0, 16);
if (raw.length < 16) return '';
let h = '';
for (let i = 0; i < 16; i += 2)
h += (parseInt(raw[i+1])<<4 | parseInt(raw[i])).toString(16).padStart(2,'0');
return '9408' + h;
},
fields: [{id:'imeisv',label:'IMEISV',type:'text',pattern:'\\d{16}',placeholder:'16 digits'}],
},
'09': { tlv: 'AD01',
decode(hex) {
const v = hex.replace(/\s/g, '').slice(4); if (v.length < 2) return null;
return { mode: parseInt(v[0]+v[1],16).toString() };
},
encode(v) {
return 'AD01' + (parseInt(v.mode)||0).toString(16).padStart(2,'0');
},
fields: [{id:'mode',label:'Search Mode',type:'select',
opts:[{v:0,l:'Automatic'},{v:1,l:'Manual'}]}],
},
'0A': { tlv: '6301',
decode(hex) {
const v = hex.replace(/\s/g, '').slice(4); if (v.length < 2) return null;
return { charge: parseInt(v[0]+v[1],16).toString() };
},
encode(v) { return '6301' + (parseInt(v.charge)||50).toString(16).padStart(2,'0'); },
fields: [{id:'charge',label:'Charge state (%)',type:'number',min:'0',max:'100',placeholder:'50'}],
},
'0E': { tlv: 'BF',
decode(hex) {
const v = hex.replace(/\s/g, '').slice(4); if (v.length < 2) return null;
const techs = []; for (let i = 0; i < v.length; i += 2) techs.push(parseInt(v[i]+v[i+1],16));
return { techs: techs.join(',') };
},
encode(v) {
const techs = (v.techs||'').split(',').filter(t => t !== '').map(t => parseInt(t)&0xFF);
if (!techs.length) return '';
const len = techs.length.toString(16).padStart(2,'0');
return 'BF' + len + techs.map(t => t.toString(16).padStart(2,'0')).join('');
},
fields: [
{id:'techs',label:'Access Technologies (comma-separated)',type:'text',
placeholder:'e.g. 3,8,10 (= UTRAN + E-UTRAN + NG-RAN)'},
],
},
};
async function pysimPliRender() {
const el = document.getElementById('pysim-pli-dict');
el.innerHTML = '<span class="text-gray-400">' + t('Loading...') + '</span>';
try {
const data = await pysimFetch('/api/pli-dict');
let html = '<div class="space-y-1">';
PLI_QUALIFIERS.forEach(q => {
const val = data[q.code] || '';
const hasCodec = !!PLI_CODECS[q.code];
html += '<div data-pli-row="' + q.code + '">'
+ '<div class="flex items-center gap-1">'
+ '<span class="w-6 text-right text-gray-400 text-[10px]">' + q.code + '</span>'
+ '<span class="w-32 truncate text-[10px]" title="' + esc(q.name) + '">' + esc(q.name) + '</span>'
+ '<input type="text" id="pli-v-' + q.code + '" value="' + esc(val) + '" placeholder="hex"'
+ ' class="w-28 border border-gray-300 dark:border-slate-600 text-[10px] rounded px-1 py-0.5 dark:bg-slate-800 font-mono">';
if (hasCodec) {
html += '<span onclick="pysimPliDecodeRow(\'' + q.code + '\')" id="pli-tog-' + q.code + '"'
+ ' class="cursor-pointer select-none text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">▶</span>';
} else {
html += '<span class="w-4"></span>';
}
html += '</div><div class="pli-decode-panel hidden mt-1 ml-8 pl-2 border-l border-gray-300 dark:border-slate-600" data-pli-panel="' + q.code + '"></div></div>';
});
html += '</div>';
el.innerHTML = html;
} catch (e) {
el.innerHTML = '<span class="text-red-500">Error: ' + esc(e.message) + '</span>';
}
}
function pysimPliDecodeRow(code) {
const panel = document.querySelector('[data-pli-panel="' + code + '"]');
const codec = PLI_CODECS[code];
if (!codec) return;
if (!panel.classList.contains('hidden')) {
panel.innerHTML = '';
panel.classList.add('hidden');
const tog = document.getElementById('pli-tog-' + code);
if (tog) tog.textContent = '▶';
return;
}
const input = document.getElementById('pli-v-' + code);
const hex = input.value.trim().replace(/\s/g, '');
let values = {};
if (hex) {
values = codec.decode(hex);
if (!values) {
panel.innerHTML = '<span class="text-[10px] text-red-500">Invalid hex</span>';
panel.classList.remove('hidden');
const tog = document.getElementById('pli-tog-' + code);
if (tog) tog.textContent = '▼';
return;
}
}
let html = '<div class="space-y-1 mb-1">';
codec.fields.forEach(f => {
html += '<div class="flex items-center gap-1">'
+ '<span class="text-[10px] w-12 text-gray-500 dark:text-slate-400 truncate" title="' + esc(f.label) + '">' + esc(f.label) + '</span>';
if (f.type === 'select') {
html += '<select id="pli-d-' + code + '-' + f.id + '" class="text-[10px] border border-gray-300 dark:border-slate-600 rounded px-1 py-0.5 dark:bg-slate-800 w-36">';
f.opts.forEach(o => html += '<option value="' + o.v + '"' + (String(values[f.id]) === String(o.v) ? ' selected' : '') + '>' + esc(o.l) + '</option>');
html += '</select>';
} else {
html += '<input id="pli-d-' + code + '-' + f.id + '" type="' + f.type + '"'
+ ' value="' + esc(String(values[f.id] || '')) + '"'
+ (f.pattern ? ' pattern="' + f.pattern + '"' : '')
+ (f.placeholder ? ' placeholder="' + f.placeholder + '"' : '')
+ (f.min !== undefined ? ' min="' + f.min + '" max="' + f.max + '"' : '')
+ ' class="text-[10px] border border-gray-300 dark:border-slate-600 rounded px-1 py-0.5 dark:bg-slate-800 w-32 font-mono">';
}
html += '</div>';
});
html += '</div>';
html += '<div class="flex gap-1">'
+ '<button onclick="pysimPliApplyRow(\'' + code + '\')" class="text-[10px] px-1.5 py-0.5 rounded bg-emerald-600 text-white hover:bg-emerald-700">Apply</button>'
+ '<button onclick="var p=document.querySelector(\'[data-pli-panel=\\\'' + code + '\\\']\');p.classList.add(\'hidden\');p.innerHTML=\'\';var t=document.getElementById(\'pli-tog-' + code + '\');if(t)t.textContent=\'▶\'" class="text-[10px] px-1.5 py-0.5 rounded border border-gray-300 dark:border-slate-600">Cancel</button>'
+ '</div>';
panel.innerHTML = html;
panel.classList.remove('hidden');
const tog = document.getElementById('pli-tog-' + code);
if (tog) tog.textContent = '▼';
}
function pysimPliApplyRow(code) {
const codec = PLI_CODECS[code];
if (!codec) return;
const values = {};
codec.fields.forEach(f => {
const el = document.getElementById('pli-d-' + code + '-' + f.id);
if (el) values[f.id] = el.value;
});
const hex = codec.encode(values);
if (!hex) return;
const input = document.getElementById('pli-v-' + code);
if (input) input.value = hex;
const panel = document.querySelector('[data-pli-panel="' + code + '"]');
if (panel) { panel.innerHTML = ''; panel.classList.add('hidden'); }
const tog = document.getElementById('pli-tog-' + code);
if (tog) tog.textContent = '▶';
}
async function pysimPliSave() {
const btn = document.getElementById('pli-save-btn');
btn.disabled = true;
btn.textContent = t('Saving...');
const data = {};
PLI_QUALIFIERS.forEach(q => {
const el = document.getElementById('pli-v-' + q.code);
if (el) {
const v = el.value.trim().replace(/\s/g, '');
if (v && !/^[0-9A-Fa-f]*$/.test(v)) return;
data[q.code] = v;
}
});
try {
await pysimFetch('/api/pli-dict', data);
btn.textContent = t('Saved');
btn.classList.add('bg-emerald-700');
setTimeout(() => { btn.textContent = t('Save'); btn.classList.remove('bg-emerald-700'); btn.disabled = false; }, 1500);
} catch (e) {
btn.textContent = 'Error';
btn.classList.add('bg-red-600');
setTimeout(() => { btn.textContent = t('Save'); btn.classList.remove('bg-red-600'); btn.disabled = false; }, 2000);
}
}
// ===== custom files =====
let pysimCustomFiles = [];
@@ -3672,10 +4355,10 @@ function pysimCustomExport() {
el.select();
}
function pysimCustomImport() {
const el = document.getElementById('pysim-cf-io');
async function pysimCustomImport() {
try {
const imported = JSON.parse(el.value);
const text = await navigator.clipboard.readText();
const imported = JSON.parse(text);
if (!Array.isArray(imported)) throw new Error('Expected an array');
let added = 0;
for (const entry of imported) {
@@ -3694,6 +4377,7 @@ function pysimCustomImport() {
}
pysimCustomSave();
pysimCustomRender();
const el = document.getElementById('pysim-cf-io');
el.value = 'Imported ' + added + ' files.';
setTimeout(() => { el.classList.add('hidden'); }, 2000);
} catch (e) {
@@ -3836,6 +4520,30 @@ const LANG_RU = {
'No events configured.': 'Нет настроенных событий.',
'Fetched proactive commands:': 'Извлечённые проактивные команды:',
'No commands logged yet.': 'Команды пока не регистрировались.',
'Send event notification to the card?': 'Отправить уведомление о событии на карту?',
'This event type is not yet supported.': 'Этот тип события пока не поддерживается.',
'Event sent.': 'Событие отправлено.',
'Event data sent.': 'Данные события отправлены.',
'Sending...': 'Отправка...',
'Invalid': 'Некорректное значение',
'Service Status': 'Статус сервиса',
'Normal service': 'Обычный сервис',
'Limited service': 'Ограниченный сервис',
'No service': 'Нет сервиса',
'Language code': 'Код языка',
'Registration type': 'Тип регистрации',
'Access Technology': 'Технология доступа',
'Rejection cause code': 'Код причины отклонения',
'Extended EMM cause (optional)': 'Расширенная EMM причина (опционально)',
'Status': 'Статус',
'Card reader removed': 'Картридер извлечён',
'Card reader inserted': 'Картридер вставлен',
'PROVIDE LOCAL INFORMATION response data': 'Данные для PROVIDE LOCAL INFORMATION',
'Send STATUS': 'Отправить STATUS',
'STATUS...': 'STATUS...',
'Saving...': 'Сохранение...',
'Saved': 'Сохранено',
'Save': 'Сохранить',
};
let currentLang = 'en';
+121
View File
@@ -590,6 +590,11 @@ video {
z-index: 50;
}
.mx-4 {
margin-left: 1rem;
margin-right: 1rem;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
@@ -655,6 +660,10 @@ video {
margin-top: 0.75rem;
}
.mt-4 {
margin-top: 1rem;
}
.block {
display: block;
}
@@ -679,14 +688,26 @@ video {
height: 4rem;
}
.max-h-20 {
max-height: 5rem;
}
.max-h-32 {
max-height: 8rem;
}
.max-h-\[60vh\] {
max-height: 60vh;
}
.max-h-\[80vh\] {
max-height: 80vh;
}
.max-h-\[85vh\] {
max-height: 85vh;
}
.min-h-20 {
min-height: 5rem;
}
@@ -719,6 +740,26 @@ video {
width: 6rem;
}
.w-28 {
width: 7rem;
}
.w-3 {
width: 0.75rem;
}
.w-32 {
width: 8rem;
}
.w-36 {
width: 9rem;
}
.w-4 {
width: 1rem;
}
.w-48 {
width: 12rem;
}
@@ -751,6 +792,10 @@ video {
max-width: 28rem;
}
.max-w-sm {
max-width: 24rem;
}
.max-w-xs {
max-width: 20rem;
}
@@ -797,6 +842,10 @@ video {
align-items: center;
}
.justify-end {
justify-content: flex-end;
}
.justify-center {
justify-content: center;
}
@@ -851,6 +900,12 @@ video {
margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
}
.space-y-3 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
}
.overflow-auto {
overflow: auto;
}
@@ -863,6 +918,12 @@ video {
overflow-y: auto;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.whitespace-nowrap {
white-space: nowrap;
}
@@ -900,6 +961,10 @@ video {
border-bottom-width: 1px;
}
.border-l {
border-left-width: 1px;
}
.border-t {
border-top-width: 1px;
}
@@ -933,6 +998,11 @@ video {
background-color: rgb(5 150 105 / var(--tw-bg-opacity, 1));
}
.bg-emerald-700 {
--tw-bg-opacity: 1;
background-color: rgb(4 120 87 / var(--tw-bg-opacity, 1));
}
.bg-gray-100 {
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
@@ -963,6 +1033,11 @@ video {
background-color: rgb(254 226 226 / var(--tw-bg-opacity, 1));
}
.bg-red-600 {
--tw-bg-opacity: 1;
background-color: rgb(220 38 38 / var(--tw-bg-opacity, 1));
}
.bg-slate-600 {
--tw-bg-opacity: 1;
background-color: rgb(71 85 105 / var(--tw-bg-opacity, 1));
@@ -989,11 +1064,20 @@ video {
padding: 1rem;
}
.p-6 {
padding: 1.5rem;
}
.px-1 {
padding-left: 0.25rem;
padding-right: 0.25rem;
}
.px-1\.5 {
padding-left: 0.375rem;
padding-right: 0.375rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
@@ -1054,6 +1138,10 @@ video {
padding-bottom: 1rem;
}
.pl-2 {
padding-left: 0.5rem;
}
.pt-1 {
padding-top: 0.25rem;
}
@@ -1087,6 +1175,15 @@ video {
line-height: 2rem;
}
.text-\[10px\] {
font-size: 10px;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
@@ -1109,6 +1206,10 @@ video {
font-weight: 400;
}
.font-semibold {
font-weight: 600;
}
.italic {
font-style: italic;
}
@@ -1117,11 +1218,21 @@ video {
line-height: 1.625;
}
.text-amber-600 {
--tw-text-opacity: 1;
color: rgb(217 119 6 / var(--tw-text-opacity, 1));
}
.text-blue-600 {
--tw-text-opacity: 1;
color: rgb(37 99 235 / var(--tw-text-opacity, 1));
}
.text-emerald-600 {
--tw-text-opacity: 1;
color: rgb(5 150 105 / var(--tw-text-opacity, 1));
}
.text-gray-300 {
--tw-text-opacity: 1;
color: rgb(209 213 219 / var(--tw-text-opacity, 1));
@@ -1334,6 +1445,11 @@ video {
color: rgb(96 165 250 / var(--tw-text-opacity, 1));
}
.dark\:text-emerald-400:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(52 211 153 / var(--tw-text-opacity, 1));
}
.dark\:text-gray-600:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity, 1));
@@ -1403,6 +1519,11 @@ video {
background-color: rgb(51 65 85 / var(--tw-bg-opacity, 1));
}
.dark\:hover\:text-gray-300:hover:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(209 213 219 / var(--tw-text-opacity, 1));
}
.dark\:hover\:text-slate-300:hover:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(203 213 225 / var(--tw-text-opacity, 1));