Add reliability enhancements, reachability pre-check, and AT presets
High-impact reliability: - SQLite job persistence (rv50x.db) — job history and AT sessions survive restarts - Extract _login_and_open_modal() — eliminates ~40 lines of duplicated Playwright login logic - Separate NocoDB view IDs per group via NOCODB_VIEW_ID_ELECTRIC / NOCODB_VIEW_ID_GW env vars - Excel cache TTL (1h) + size cap (20 files) with eviction helpers - In-memory job store pruning (MAX_JOBS_MEMORY, default 200) Functionality: - TCP reachability pre-check before launching Playwright — fails fast on unreachable devices - AT command presets — save/load/delete named command sequences, stored in at_presets.json Ops: - Bind-mount rv50x.db and at_presets.json in docker-compose so data survives rebuilds - Add NOCODB_VIEW_ID_ELECTRIC, NOCODB_VIEW_ID_GW, REACH_TIMEOUT env vars to compose - Ignore runtime files (rv50x.db, at_presets.json, template dirs) in .gitignore Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+75
-1
@@ -238,6 +238,12 @@
|
||||
.at-msg.info { color: var(--text2); }
|
||||
.at-sep { border: none; border-top: 1px solid var(--border); margin: 8px 0; }
|
||||
.at-results-bar { display: flex; gap: 20px; padding: 8px 20px; background: var(--bg2); border-top: 1px solid var(--border); font-family: var(--font-mono); font-size: 11px; }
|
||||
.preset-strip { display: flex; flex-wrap: wrap; gap: 5px; min-height: 24px; }
|
||||
.preset-pill { display: inline-flex; align-items: center; background: rgba(255,109,0,0.08); border: 1px solid rgba(255,109,0,0.3); border-radius: 3px; font-family: var(--font-mono); font-size: 10px; }
|
||||
.preset-load { background: none; border: none; color: var(--orange); cursor: pointer; font-family: var(--font-mono); font-size: 10px; padding: 3px 6px; letter-spacing: 0.3px; }
|
||||
.preset-load:hover { color: var(--text); }
|
||||
.preset-del { background: none; border: none; color: var(--text3); cursor: pointer; font-size: 13px; line-height: 1; padding: 2px 5px 2px 0; }
|
||||
.preset-del:hover { color: var(--red); }
|
||||
.btn-orange { background: rgba(255,109,0,0.12); border: 1px solid rgba(255,109,0,0.5); color: var(--orange); }
|
||||
.btn-orange:hover:not(:disabled) { background: rgba(255,109,0,0.2); box-shadow: 0 0 16px rgba(255,109,0,0.2); }
|
||||
|
||||
@@ -737,6 +743,16 @@
|
||||
<button class="at-quick" onclick="atSetCmd('AT*WWAN1HOMEPAGEURL?')">APN URL</button>
|
||||
<button class="at-quick" onclick="atSetCmd('ATI1\nAT*CELLINFO2?\nAT*NETRSSI?')">📶 signal bundle</button>
|
||||
</div>
|
||||
<!-- Saved presets -->
|
||||
<div style="margin-top:10px;padding-top:8px;border-top:1px solid var(--border)">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
|
||||
<span style="font-family:'Share Tech Mono',monospace;font-size:10px;color:var(--text3);letter-spacing:1.5px">PRESETS</span>
|
||||
<button class="at-quick" onclick="atSavePreset()" style="margin-left:auto;padding:2px 8px">+ SAVE CURRENT</button>
|
||||
</div>
|
||||
<div class="preset-strip" id="at-preset-strip">
|
||||
<span style="font-family:'Share Tech Mono',monospace;font-size:10px;color:var(--text3)">No presets saved</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:10px;margin-top:10px;align-items:center">
|
||||
<button class="btn btn-orange" id="at-send-btn" onclick="atSend()">▶ SEND</button>
|
||||
<button class="btn btn-neutral btn-sm" onclick="atClearTerminal()">CLEAR LOG</button>
|
||||
@@ -1449,6 +1465,64 @@ async function atSend() {
|
||||
};
|
||||
}
|
||||
|
||||
// ── AT Presets ────────────────────────────────────────────────────────────────
|
||||
async function atLoadPresets() {
|
||||
try {
|
||||
const res = await fetch('/api/at/presets');
|
||||
const data = await res.json();
|
||||
atRenderPresets(data.presets || []);
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
function atRenderPresets(presets) {
|
||||
const strip = document.getElementById('at-preset-strip');
|
||||
if (!presets.length) {
|
||||
strip.innerHTML = '<span style="font-family:\'Share Tech Mono\',monospace;font-size:10px;color:var(--text3)">No presets saved</span>';
|
||||
return;
|
||||
}
|
||||
strip.innerHTML = presets.map(p => `
|
||||
<span class="preset-pill">
|
||||
<button class="preset-load"
|
||||
onclick="atApplyPreset(${JSON.stringify(p.commands)})"
|
||||
title="${escHtml(p.commands.join(' | '))}">${escHtml(p.name)}</button>
|
||||
<button class="preset-del"
|
||||
onclick="atDeletePreset(${JSON.stringify(p.name)})"
|
||||
title="Delete preset">×</button>
|
||||
</span>`).join('');
|
||||
}
|
||||
|
||||
function atApplyPreset(commands) {
|
||||
document.getElementById('at-cmd-input').value = commands.join('\n');
|
||||
}
|
||||
|
||||
async function atSavePreset() {
|
||||
const rawCmds = document.getElementById('at-cmd-input').value
|
||||
.split('\n').map(l => l.trim()).filter(l => l);
|
||||
if (!rawCmds.length) { alert('Enter at least one command in the textarea first.'); return; }
|
||||
const name = prompt('Preset name:');
|
||||
if (!name || !name.trim()) return;
|
||||
try {
|
||||
const res = await fetch('/api/at/presets', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: name.trim(), commands: rawCmds }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.error) { alert('Error: ' + data.error); return; }
|
||||
await atLoadPresets();
|
||||
} catch(e) { alert('Request failed: ' + e.message); }
|
||||
}
|
||||
|
||||
async function atDeletePreset(name) {
|
||||
if (!confirm(`Delete preset "${name}"?`)) return;
|
||||
try {
|
||||
const res = await fetch(`/api/at/presets/${encodeURIComponent(name)}`, { method: 'DELETE' });
|
||||
const data = await res.json();
|
||||
if (data.error) { alert('Error: ' + data.error); return; }
|
||||
await atLoadPresets();
|
||||
} catch(e) { alert('Request failed: ' + e.message); }
|
||||
}
|
||||
|
||||
// ── Tab switching ──────────────────────────────────────────────────────────────
|
||||
function switchTab(name,btn){
|
||||
document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
|
||||
@@ -1457,7 +1531,7 @@ function switchTab(name,btn){
|
||||
document.getElementById(`tab-${name}`).classList.add('active');
|
||||
if(name==='history') refreshJobList();
|
||||
if(name==='builder'){ loadBuilderTemplates(); if(builderSource==='nocodb') loadBuilderHostnames(); }
|
||||
if(name==='at'){ atDevices=[...allDevices].sort((a,b)=>a.id.localeCompare(b.id)); atFiltered=atDevices.slice(); atRenderDeviceList(); }
|
||||
if(name==='at'){ atDevices=[...allDevices].sort((a,b)=>a.id.localeCompare(b.id)); atFiltered=atDevices.slice(); atRenderDeviceList(); atLoadPresets(); }
|
||||
}
|
||||
function switchTabById(name){
|
||||
document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
|
||||
|
||||
Reference in New Issue
Block a user