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
| Datum | Quellen | Ziel | +Dateien | Größe | +Dauer | Status | +
|---|---|---|---|---|---|---|
| ${date} ${time} |
+ ${srcs} | +${e.dest||'?'} | +${files}${io} | +${size} | +${fmtDur(e.duration||0)} | +${status} | +