feat: Kopier-Verlauf hinzugefügt und Systeminformationen bereitgestellt; Versionsnummer auf 1.0.57 erhöht

This commit is contained in:
2026-05-09 19:31:02 +02:00
parent 1a2b8a6516
commit 9b8fdb411c
2 changed files with 228 additions and 3 deletions

229
app.py
View File

@@ -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">&nbsp;</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()">🔍&nbsp;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()">&#8634;&nbsp;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()">✕&nbsp;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,'&quot;')}">✗ 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>

View File

@@ -1 +1 @@
1.0.56
1.0.57