From 9b8fdb411cab0ac2e53e866a26d9da1d00c86921 Mon Sep 17 00:00:00 2001 From: Tobias Leuschner Date: Sat, 9 May 2026 19:31:02 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Kopier-Verlauf=20hinzugef=C3=BCgt=20und?= =?UTF-8?q?=20Systeminformationen=20bereitgestellt;=20Versionsnummer=20auf?= =?UTF-8?q?=201.0.57=20erh=C3=B6ht?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 229 +++++++++++++++++++++++++++++++++++++++++++++++++++- version.txt | 2 +- 2 files changed, 228 insertions(+), 3 deletions(-) diff --git a/app.py b/app.py index 21e77c4..84d92d9 100644 --- a/app.py +++ b/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 System
+
+
+
CPU-Temp
+
--
+
 
+
+
+
RAM
+
--
+
+
+
+
SD-Karte
+
--
+
+
+
+ +
+
+
📋
+ Kopier-Verlauf + +
+
+
Noch keine Kopiervorgänge gespeichert.
+
+
+
@@ -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='
Noch keine Kopiervorgänge gespeichert.
'; + return; + } + w.innerHTML=` + + + + + + ${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?` (+${e.skipped} übersp.)`:''); + const size=e.bytes>0?fmtBytes(e.bytes):'--'; + const status=e.ok + ? '✓ OK' + : `✗ Fehler`; + const io=e.errors?` ${e.errors} I/O-Err.`:''; + return` + + + + + + + + `; + }).join('')} +
DatumQuellenZielDateienGrößeDauerStatus
${date}
${time}
${srcs}${e.dest||'?'}${files}${io}${size}${fmtDur(e.duration||0)}${status}
`; +} +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); })(); diff --git a/version.txt b/version.txt index 3c79fcb..91cc3fe 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.0.56 \ No newline at end of file +1.0.57 \ No newline at end of file