feat: Kopier-Verlauf hinzugefügt und Systeminformationen bereitgestellt; Versionsnummer auf 1.0.57 erhöht
This commit is contained in:
229
app.py
229
app.py
@@ -40,6 +40,8 @@ LOG_DIR = BASE_DIR / 'logs'
|
||||
LOG_FILE = LOG_DIR / 'picopy.log'
|
||||
INTERNAL_DEST_DIR = BASE_DIR / 'internal'
|
||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
HISTORY_FILE = BASE_DIR / 'history.json'
|
||||
MAX_HISTORY = 100
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
@@ -107,6 +109,64 @@ def save_state():
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# -- Kopier-Verlauf ----------------------------------------------------------
|
||||
|
||||
def load_history() -> list:
|
||||
try:
|
||||
if HISTORY_FILE.exists():
|
||||
return json.loads(HISTORY_FILE.read_text(encoding='utf-8'))
|
||||
except Exception:
|
||||
pass
|
||||
return []
|
||||
|
||||
def append_history(entry: dict):
|
||||
h = load_history()
|
||||
h.insert(0, entry)
|
||||
try:
|
||||
_atomic_write(HISTORY_FILE, json.dumps(h[:MAX_HISTORY]))
|
||||
except Exception as e:
|
||||
log.warning(f'Verlauf speichern fehlgeschlagen: {e}')
|
||||
|
||||
|
||||
# -- Systeminfo --------------------------------------------------------------
|
||||
|
||||
def get_sysinfo() -> dict:
|
||||
info: dict = {}
|
||||
# CPU-Temperatur (Raspberry Pi)
|
||||
for zone in ('/sys/class/thermal/thermal_zone0/temp',
|
||||
'/sys/class/thermal/thermal_zone1/temp'):
|
||||
try:
|
||||
raw = Path(zone).read_text().strip()
|
||||
info['cpu_temp'] = round(int(raw) / 1000, 1)
|
||||
break
|
||||
except Exception:
|
||||
info['cpu_temp'] = None
|
||||
# RAM
|
||||
try:
|
||||
mem: dict = {}
|
||||
for line in Path('/proc/meminfo').read_text().splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
mem[parts[0].rstrip(':')] = int(parts[1])
|
||||
total = mem.get('MemTotal', 0)
|
||||
avail = mem.get('MemAvailable', 0)
|
||||
used = total - avail
|
||||
info['ram_total'] = round(total / 1024)
|
||||
info['ram_used'] = round(used / 1024)
|
||||
info['ram_pct'] = round(used / total * 100) if total else 0
|
||||
except Exception:
|
||||
info['ram_total'] = info['ram_used'] = info['ram_pct'] = None
|
||||
# SD-Karte (root-Dateisystem)
|
||||
try:
|
||||
du = shutil.disk_usage('/')
|
||||
info['disk_total'] = round(du.total / 1e9, 1)
|
||||
info['disk_used'] = round(du.used / 1e9, 1)
|
||||
info['disk_pct'] = round(du.used / du.total * 100) if du.total else 0
|
||||
except Exception:
|
||||
info['disk_total'] = info['disk_used'] = info['disk_pct'] = None
|
||||
return info
|
||||
|
||||
|
||||
# -- WiFi Status -------------------------------------------------------------
|
||||
|
||||
wifi_state = {
|
||||
@@ -761,6 +821,11 @@ def do_copy(src_devs, dst_dev, cfg):
|
||||
dst_owned = False
|
||||
src_mounts = [] # [(src_dev, src_mp, src_owned)]
|
||||
_upload_thread = None
|
||||
_hist = {
|
||||
'start': time.time(),
|
||||
'ok': False, 'copied': 0, 'skipped': 0, 'errors': 0,
|
||||
'bytes': 0, 'error_msg': '',
|
||||
}
|
||||
try:
|
||||
with copy_lock:
|
||||
copy_state.update(running=True, progress=0, error=None,
|
||||
@@ -978,6 +1043,9 @@ def do_copy(src_devs, dst_dev, cfg):
|
||||
|
||||
with copy_lock:
|
||||
copy_state['last_copy'] = datetime.now().isoformat()
|
||||
_hist['bytes'] = copy_state['bytes_done']
|
||||
_hist.update(ok=True, copied=len(all_copied_pairs),
|
||||
skipped=skipped, errors=io_errors)
|
||||
add_log('Fertig! ' + ', '.join(msg_parts))
|
||||
|
||||
dst_dir_root = Path(dst_mp) / date_str
|
||||
@@ -996,6 +1064,7 @@ def do_copy(src_devs, dst_dev, cfg):
|
||||
log.exception('Copy failed')
|
||||
with copy_lock:
|
||||
copy_state['error'] = str(e)
|
||||
_hist['error_msg'] = str(e)
|
||||
add_log(f'Fehler: {e}')
|
||||
|
||||
finally:
|
||||
@@ -1014,6 +1083,19 @@ def do_copy(src_devs, dst_dev, cfg):
|
||||
copy_state['current'] = ''
|
||||
copy_state['phase'] = 'idle'
|
||||
save_state()
|
||||
# Verlaufseintrag speichern
|
||||
append_history({
|
||||
'ts': datetime.now().isoformat(),
|
||||
'duration': int(time.time() - _hist['start']),
|
||||
'sources': [d.get('label', d.get('device', '?')) for d in src_devs],
|
||||
'dest': dst_dev.get('label', dst_dev.get('device', '?')) if dst_dev else '?',
|
||||
'copied': _hist['copied'],
|
||||
'skipped': _hist['skipped'],
|
||||
'errors': _hist['errors'],
|
||||
'bytes': _hist['bytes'],
|
||||
'ok': _hist['ok'],
|
||||
'error': _hist['error_msg'],
|
||||
})
|
||||
|
||||
def check_auto_copy():
|
||||
cfg = load_cfg()
|
||||
@@ -1435,6 +1517,22 @@ def r_config():
|
||||
return jsonify(ok=True)
|
||||
return jsonify(load_cfg())
|
||||
|
||||
@app.route('/api/history')
|
||||
def r_history():
|
||||
return jsonify(load_history())
|
||||
|
||||
@app.route('/api/history', methods=['DELETE'])
|
||||
def r_history_clear():
|
||||
try:
|
||||
HISTORY_FILE.write_text('[]', encoding='utf-8')
|
||||
except Exception:
|
||||
pass
|
||||
return jsonify(ok=True)
|
||||
|
||||
@app.route('/api/sysinfo')
|
||||
def r_sysinfo():
|
||||
return jsonify(get_sysinfo())
|
||||
|
||||
@app.route('/api/status')
|
||||
def r_status():
|
||||
with copy_lock:
|
||||
@@ -2108,6 +2206,20 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
|
||||
|
||||
/* -- Log -- */
|
||||
.log-wrap{font-family:ui-monospace,monospace;font-size:.75rem;max-height:300px;overflow-y:auto;background:var(--bg2);border-radius:.45rem;padding:.5rem}
|
||||
.si-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:.6rem;margin-bottom:.6rem}
|
||||
.si-item{background:var(--bg2);border-radius:.45rem;padding:.55rem .7rem}
|
||||
.si-label{font-size:.7rem;color:var(--sub);text-transform:uppercase;letter-spacing:.04em;margin-bottom:.2rem}
|
||||
.si-val{font-size:1.05rem;font-weight:700;color:var(--txt)}
|
||||
.si-sub{font-size:.7rem;color:var(--sub);margin-top:.1rem}
|
||||
.si-bar{height:4px;background:var(--brd);border-radius:9999px;margin-top:.35rem;overflow:hidden}
|
||||
.si-fill{height:100%;border-radius:9999px;transition:width .5s}
|
||||
.si-fill.ok{background:var(--grn2)}.si-fill.warn{background:var(--ylw)}.si-fill.hot{background:var(--red)}
|
||||
.hist-table{width:100%;border-collapse:collapse;font-size:.8rem}
|
||||
.hist-table th{text-align:left;padding:.35rem .6rem;color:var(--sub);font-weight:600;font-size:.72rem;text-transform:uppercase;letter-spacing:.04em;border-bottom:1px solid var(--brd);white-space:nowrap}
|
||||
.hist-table td{padding:.42rem .6rem;border-bottom:1px solid var(--brd);vertical-align:middle}
|
||||
.hist-table tr:last-child td{border-bottom:none}
|
||||
.hist-table tr:hover td{background:var(--bg2)}
|
||||
.hist-ok{color:var(--grn);font-weight:700}.hist-err{color:var(--red);font-weight:700}
|
||||
.log-row{display:flex;gap:.5rem;padding:.18rem 0;border-bottom:1px solid rgba(42,54,80,.5)}
|
||||
.log-row:last-child{border-bottom:none}
|
||||
.log-t{color:var(--brd2);flex-shrink:0}
|
||||
@@ -2562,12 +2674,41 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
|
||||
<span class="card-title">System</span>
|
||||
</div>
|
||||
<div class="card-body" style="display:flex;flex-direction:column;gap:.6rem">
|
||||
<div class="si-grid" id="sysinfo-grid">
|
||||
<div class="si-item">
|
||||
<div class="si-label">CPU-Temp</div>
|
||||
<div class="si-val" id="si-temp">--</div>
|
||||
<div class="si-sub" id="si-temp-sub"> </div>
|
||||
</div>
|
||||
<div class="si-item">
|
||||
<div class="si-label">RAM</div>
|
||||
<div class="si-val" id="si-ram">--</div>
|
||||
<div class="si-bar"><div class="si-fill ok" id="si-ram-bar" style="width:0%"></div></div>
|
||||
</div>
|
||||
<div class="si-item">
|
||||
<div class="si-label">SD-Karte</div>
|
||||
<div class="si-val" id="si-disk">--</div>
|
||||
<div class="si-bar"><div class="si-fill ok" id="si-disk-bar" style="width:0%"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn" style="width:100%" onclick="checkUpdate()">🔍 Nach Update suchen</button>
|
||||
<div id="sys-update-flash" class="flash" style="display:none"></div>
|
||||
<button class="btn" style="width:100%;background:rgba(220,60,60,.12);color:#e05555;border-color:rgba(220,60,60,.25)" onclick="rebootDevice()">↺ Gerät neu starten</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- -- Kopier-Verlauf -- -->
|
||||
<div class="card col2">
|
||||
<div class="card-head">
|
||||
<div class="card-icon" style="background:rgba(79,142,247,.1);color:var(--acc)">📋</div>
|
||||
<span class="card-title">Kopier-Verlauf</span>
|
||||
<button class="btn sm ghost danger" style="margin-left:auto" onclick="clearHistory()">✕ Löschen</button>
|
||||
</div>
|
||||
<div class="card-body" style="padding:.5rem .75rem">
|
||||
<div id="history-wrap"><div class="expl-empty" style="padding:.75rem 0">Noch keine Kopiervorgänge gespeichert.</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- -- Logs -- -->
|
||||
<div class="card col2">
|
||||
<div class="card-head">
|
||||
@@ -3248,6 +3389,7 @@ async function poll(){
|
||||
pf.className='prog-fill err'; pw.style.display='block'; pf.style.width='100%';
|
||||
sum.textContent=''; time.textContent='';
|
||||
}else if(c.last_copy && !_dismissed){
|
||||
if(c.last_copy !== _lastHistoryTs){ _lastHistoryTs=c.last_copy; loadHistory(); }
|
||||
tx.className='st-headline st-ok'; tx.textContent='✓ Abgeschlossen';
|
||||
pf.className='prog-fill done'; pw.style.display='block'; pf.style.width='100%';
|
||||
sum.textContent=c.total+' Dateien'+' | '+fmtBytes(c.bytes_total);
|
||||
@@ -3294,7 +3436,7 @@ async function poll(){
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
let _dismissed = false, _autoDismissTimer = null;
|
||||
let _dismissed = false, _autoDismissTimer = null, _lastHistoryTs = null;
|
||||
function dismissStatus(){
|
||||
_dismissed = true;
|
||||
if(_autoDismissTimer){ clearTimeout(_autoDismissTimer); _autoDismissTimer=null; }
|
||||
@@ -3430,18 +3572,101 @@ function flash(id,cls,msg){
|
||||
if(cls==='ok') setTimeout(()=>el.style.display='none',3500);
|
||||
}
|
||||
|
||||
// -- Sysinfo ------------------------------------------------------------------
|
||||
async function pollSysinfo(){
|
||||
try{
|
||||
const s=await api('/sysinfo');
|
||||
// CPU-Temp
|
||||
const tempEl=$('si-temp'), tempSub=$('si-temp-sub');
|
||||
if(s.cpu_temp!=null){
|
||||
tempEl.textContent=s.cpu_temp+'°C';
|
||||
const cls=s.cpu_temp>=80?'hot':s.cpu_temp>=65?'warn':'ok';
|
||||
tempEl.style.color=cls==='hot'?'var(--red)':cls==='warn'?'var(--ylw)':'var(--grn)';
|
||||
tempSub.textContent=s.cpu_temp>=80?'Heiß':s.cpu_temp>=65?'Warm':'Normal';
|
||||
} else { tempEl.textContent='n/v'; tempSub.textContent=''; }
|
||||
// RAM
|
||||
if(s.ram_used!=null){
|
||||
$('si-ram').textContent=s.ram_used+' / '+s.ram_total+' MB';
|
||||
const rb=$('si-ram-bar'); rb.style.width=s.ram_pct+'%';
|
||||
rb.className='si-fill '+(s.ram_pct>=90?'hot':s.ram_pct>=70?'warn':'ok');
|
||||
}
|
||||
// Disk
|
||||
if(s.disk_used!=null){
|
||||
$('si-disk').textContent=s.disk_used+' / '+s.disk_total+' GB';
|
||||
const db=$('si-disk-bar'); db.style.width=s.disk_pct+'%';
|
||||
db.className='si-fill '+(s.disk_pct>=90?'hot':s.disk_pct>=75?'warn':'ok');
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
// -- Kopier-Verlauf -----------------------------------------------------------
|
||||
function fmtDur(s){
|
||||
if(s<60) return s+'s';
|
||||
const m=Math.floor(s/60), sec=s%60;
|
||||
return m+'m'+(sec?sec+'s':'');
|
||||
}
|
||||
async function loadHistory(){
|
||||
try{
|
||||
const h=await api('/history');
|
||||
renderHistory(h);
|
||||
}catch(e){}
|
||||
}
|
||||
function renderHistory(h){
|
||||
const w=$('history-wrap');
|
||||
if(!h||!h.length){
|
||||
w.innerHTML='<div class="expl-empty" style="padding:.75rem 0">Noch keine Kopiervorgänge gespeichert.</div>';
|
||||
return;
|
||||
}
|
||||
w.innerHTML=`<table class="hist-table">
|
||||
<thead><tr>
|
||||
<th>Datum</th><th>Quellen</th><th>Ziel</th>
|
||||
<th style="text-align:right">Dateien</th><th style="text-align:right">Größe</th>
|
||||
<th style="text-align:right">Dauer</th><th>Status</th>
|
||||
</tr></thead>
|
||||
<tbody>${h.map(e=>{
|
||||
const d=new Date(e.ts);
|
||||
const date=d.toLocaleDateString('de-DE',{day:'2-digit',month:'2-digit',year:'2-digit'});
|
||||
const time=d.toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'});
|
||||
const srcs=(e.sources||[]).join(', ')||'?';
|
||||
const files=e.copied+(e.skipped?` <span style="color:var(--sub);font-size:.75em">(+${e.skipped} übersp.)</span>`:'');
|
||||
const size=e.bytes>0?fmtBytes(e.bytes):'--';
|
||||
const status=e.ok
|
||||
? '<span class="hist-ok">✓ OK</span>'
|
||||
: `<span class="hist-err" title="${(e.error||'').replace(/"/g,'"')}">✗ Fehler</span>`;
|
||||
const io=e.errors?` <span style="color:var(--red);font-size:.75em">${e.errors} I/O-Err.</span>`:'';
|
||||
return`<tr>
|
||||
<td><span style="font-weight:600">${date}</span><br><span style="color:var(--sub);font-size:.75em">${time}</span></td>
|
||||
<td>${srcs}</td>
|
||||
<td style="color:var(--sub)">${e.dest||'?'}</td>
|
||||
<td style="text-align:right">${files}${io}</td>
|
||||
<td style="text-align:right">${size}</td>
|
||||
<td style="text-align:right">${fmtDur(e.duration||0)}</td>
|
||||
<td>${status}</td>
|
||||
</tr>`;
|
||||
}).join('')}</tbody>
|
||||
</table>`;
|
||||
}
|
||||
async function clearHistory(){
|
||||
if(!confirm('Kopier-Verlauf wirklich löschen?'))return;
|
||||
await api('/history','DELETE');
|
||||
renderHistory([]);
|
||||
}
|
||||
|
||||
(async()=>{
|
||||
await loadCfg();
|
||||
await refreshDevices();
|
||||
await loadUTs();
|
||||
await loadWgConfig();
|
||||
expl.load('');
|
||||
loadHistory();
|
||||
pollSysinfo();
|
||||
setInterval(poll,1500);
|
||||
setInterval(refreshDevices,8000);
|
||||
setInterval(pollUpdate,60000);
|
||||
setInterval(pollSysinfo,8000);
|
||||
poll();
|
||||
pollUpdate();
|
||||
setTimeout(pollUpdate, 8000); // Server-Check-Ergebnis abholen bevor der 60s-Takt greift
|
||||
setTimeout(pollUpdate, 8000);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user