feat: Versionsnummer auf 1.0.20 erhöht und Kommentare im Code aktualisiert

This commit is contained in:
2026-05-09 11:26:54 +02:00
parent 631cf21195
commit 646858267b
2 changed files with 72 additions and 72 deletions

142
app.py
View File

@@ -65,7 +65,7 @@ DEFAULT_CONFIG = {
'wireguard_auto': False, 'wireguard_auto': False,
} }
# ── Persistenter Kopierstatus ─────────────────────────────────────────────── # -- Persistenter Kopierstatus -----------------------------------------------
copy_state = { copy_state = {
'running': False, 'progress': 0, 'running': False, 'progress': 0,
@@ -101,7 +101,7 @@ def save_state():
except Exception: except Exception:
pass pass
# ── WiFi Status ───────────────────────────────────────────────────────────── # -- WiFi Status -------------------------------------------------------------
wifi_state = { wifi_state = {
'mode': 'unknown', # 'client' | 'ap' | 'disconnected' 'mode': 'unknown', # 'client' | 'ap' | 'disconnected'
@@ -110,7 +110,7 @@ wifi_state = {
} }
wifi_lock = threading.Lock() wifi_lock = threading.Lock()
# ── Config ─────────────────────────────────────────────────────────────────── # -- Config -------------------------------------------------------------------
def load_cfg(): def load_cfg():
cfg = DEFAULT_CONFIG.copy() cfg = DEFAULT_CONFIG.copy()
@@ -128,7 +128,7 @@ def load_cfg():
def save_cfg(cfg): def save_cfg(cfg):
_atomic_write(CONFIG_FILE, json.dumps(cfg, indent=2)) _atomic_write(CONFIG_FILE, json.dumps(cfg, indent=2))
# ── WiFi Hilfsfunktionen ───────────────────────────────────────────────────── # -- WiFi Hilfsfunktionen -----------------------------------------------------
def nm(*args): def nm(*args):
return subprocess.run(['nmcli'] + list(args), return subprocess.run(['nmcli'] + list(args),
@@ -216,7 +216,7 @@ def scan_wifi_networks():
nets.append({'ssid': ssid, 'signal': int(signal) if signal.isdigit() else 0, 'security': security}) nets.append({'ssid': ssid, 'signal': int(signal) if signal.isdigit() else 0, 'security': security})
return sorted(nets, key=lambda x: -x['signal']) return sorted(nets, key=lambda x: -x['signal'])
# ── WiFi Monitor Thread ─────────────────────────────────────────────────────── # -- WiFi Monitor Thread -------------------------------------------------------
def update_wifi_state(): def update_wifi_state():
info = get_wlan0_info() info = get_wlan0_info()
@@ -271,7 +271,7 @@ def wifi_monitor():
time.sleep(30) time.sleep(30)
# ── WireGuard VPN ───────────────────────────────────────────────────────────── # -- WireGuard VPN -------------------------------------------------------------
WG_CONF = Path('/etc/wireguard/picopy.conf') WG_CONF = Path('/etc/wireguard/picopy.conf')
WG_IFACE = 'picopy' WG_IFACE = 'picopy'
@@ -411,7 +411,7 @@ def wg_monitor():
time.sleep(10) time.sleep(10)
# ── USB Geräteerkennung ─────────────────────────────────────────────────────── # -- USB Geräteerkennung -------------------------------------------------------
def usb_port_of(dev_name): def usb_port_of(dev_name):
"""Gibt den physischen USB-Port-Pfad zurück (z.B. '2-2'). """Gibt den physischen USB-Port-Pfad zurück (z.B. '2-2').
@@ -491,7 +491,7 @@ def ensure_mount(dev_info):
return None, False return None, False
return mp, True return mp, True
# ── Kopier-Logik ────────────────────────────────────────────────────────────── # -- Kopier-Logik --------------------------------------------------------------
def add_log(msg): def add_log(msg):
log.info(msg) log.info(msg)
@@ -619,7 +619,7 @@ def do_copy(src_dev, dst_dev, cfg):
'source': src_dev.get('label', ''), 'source': src_dev.get('label', ''),
})) }))
# ── Dateien sammeln & filtern ────────────────────────────────────── # -- Dateien sammeln & filtern --------------------------------------
src_path = Path(src_mp) src_path = Path(src_mp)
all_files = [f for f in src_path.rglob('*') if f.is_file()] all_files = [f for f in src_path.rglob('*') if f.is_file()]
files = [f for f in all_files if _should_copy(f, cfg)] files = [f for f in all_files if _should_copy(f, cfg)]
@@ -640,7 +640,7 @@ def do_copy(src_dev, dst_dev, cfg):
skipped = 0 skipped = 0
io_errors = 0 io_errors = 0
# ── Phase 1: Kopieren ────────────────────────────────────────────── # -- Phase 1: Kopieren ----------------------------------------------
for i, f in enumerate(files): for i, f in enumerate(files):
with copy_lock: with copy_lock:
cancelled = not copy_state['running'] cancelled = not copy_state['running']
@@ -713,7 +713,7 @@ def do_copy(src_dev, dst_dev, cfg):
if io_errors: if io_errors:
msg_parts.append(f'{io_errors} Fehler (I/O)') msg_parts.append(f'{io_errors} Fehler (I/O)')
# ── Phase 2: Verifizieren ────────────────────────────────────────── # -- Phase 2: Verifizieren ------------------------------------------
verify_errors = 0 verify_errors = 0
verified_pairs = list(copied_pairs) verified_pairs = list(copied_pairs)
@@ -749,7 +749,7 @@ def do_copy(src_dev, dst_dev, cfg):
else: else:
add_log(f'Alle {len(verified_pairs)} Dateien verifiziert ✓') add_log(f'Alle {len(verified_pairs)} Dateien verifiziert ✓')
# ── Phase 3: Quelle löschen ──────────────────────────────────────── # -- Phase 3: Quelle löschen ----------------------------------------
if cfg.get('delete_source') and verified_pairs: if cfg.get('delete_source') and verified_pairs:
if verify_errors: if verify_errors:
add_log('Quelldateien NICHT gelöscht (Prüfsummenfehler)') add_log('Quelldateien NICHT gelöscht (Prüfsummenfehler)')
@@ -825,7 +825,7 @@ def usb_monitor():
except ImportError: except ImportError:
log.warning('pyudev nicht verfügbar') log.warning('pyudev nicht verfügbar')
# ── Upload-Ziele (rclone) ───────────────────────────────────────────────────── # -- Upload-Ziele (rclone) -----------------------------------------------------
RCLONE_CONF = BASE_DIR / 'rclone.conf' RCLONE_CONF = BASE_DIR / 'rclone.conf'
@@ -892,7 +892,7 @@ def run_uploads(local_dir: Path, cfg: dict):
with upload_lock: with upload_lock:
upload_state['current'] = name upload_state['current'] = name
add_log(f'Upload {name}...') add_log(f'Upload -> {name}...')
dest_root = t.get('dest_path', 'PiCopy').strip('/') dest_root = t.get('dest_path', 'PiCopy').strip('/')
dest = f'{_remote_name(t["id"])}:{dest_root}' dest = f'{_remote_name(t["id"])}:{dest_root}'
@@ -913,7 +913,7 @@ def run_uploads(local_dir: Path, cfg: dict):
upload_state['current'] = '' upload_state['current'] = ''
# ── Flask Routes ────────────────────────────────────────────────────────────── # -- Flask Routes --------------------------------------------------------------
@app.route('/') @app.route('/')
def index(): def index():
@@ -1030,7 +1030,7 @@ def r_wifi_status():
return jsonify(dict(wifi_state)) return jsonify(dict(wifi_state))
# ── WireGuard Routes ───────────────────────────────────────────────────────── # -- WireGuard Routes ---------------------------------------------------------
@app.route('/api/wireguard/config', methods=['GET', 'POST']) @app.route('/api/wireguard/config', methods=['GET', 'POST'])
def r_wg_config(): def r_wg_config():
@@ -1087,7 +1087,7 @@ def r_wg_uninstall():
return jsonify(ok=True) return jsonify(ok=True)
# ── Upload Routes ────────────────────────────────────────────────────────────── # -- Upload Routes --------------------------------------------------------------
@app.route('/api/upload/targets', methods=['GET']) @app.route('/api/upload/targets', methods=['GET'])
def r_upload_list(): def r_upload_list():
@@ -1155,7 +1155,7 @@ def r_upload_status():
return jsonify(dict(upload_state)) return jsonify(dict(upload_state))
# ── Browse (persistente Mounts für File-Explorer) ───────────────────────────── # -- Browse (persistente Mounts für File-Explorer) -----------------------------
_browse_mounts = {} # usb_port -> mount_point _browse_mounts = {} # usb_port -> mount_point
@@ -1193,7 +1193,7 @@ def get_browse_mp(dev):
if mp: if mp:
if _mp_is_alive(mp): if _mp_is_alive(mp):
return mp return mp
_drop_browse_mount(port) # veraltet aufräumen _drop_browse_mount(port) # veraltet -> aufräumen
# Frisch mounten # Frisch mounten
mp = f'/mnt/picopy_br_{port}' mp = f'/mnt/picopy_br_{port}'
@@ -1258,7 +1258,7 @@ def r_browse():
# ── Update-System ───────────────────────────────────────────────────────────── # -- Update-System -------------------------------------------------------------
update_state = { update_state = {
'current': VERSION, 'current': VERSION,
@@ -1293,7 +1293,7 @@ def check_for_updates():
update_state.update(latest=latest, available=avail, update_state.update(latest=latest, available=avail,
last_checked=datetime.now().isoformat()) last_checked=datetime.now().isoformat())
if avail: if avail:
log.info(f'Update verfügbar: {VERSION} {latest}') log.info(f'Update verfügbar: {VERSION} -> {latest}')
except Exception as e: except Exception as e:
with update_lock: with update_lock:
update_state['error'] = str(e) update_state['error'] = str(e)
@@ -1366,7 +1366,7 @@ def r_update_install():
return jsonify(error=str(e)), 500 return jsonify(error=str(e)), 500
# ── HTML Template ───────────────────────────────────────────────────────────── # -- HTML Template -------------------------------------------------------------
HTML = r"""<!DOCTYPE html> HTML = r"""<!DOCTYPE html>
<html lang="de"> <html lang="de">
@@ -1375,7 +1375,7 @@ HTML = r"""<!DOCTYPE html>
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<title>PiCopy</title> <title>PiCopy</title>
<style> <style>
/* ── Reset & Tokens ── */ /* -- Reset & Tokens -- */
:root { :root {
--bg: #0a0f1e; --bg: #0a0f1e;
--bg2: #111827; --bg2: #111827;
@@ -1398,7 +1398,7 @@ HTML = r"""<!DOCTYPE html>
*{box-sizing:border-box;margin:0;padding:0} *{box-sizing:border-box;margin:0;padding:0}
body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;min-height:100vh;padding:0 0 4rem} body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;min-height:100vh;padding:0 0 4rem}
/* ── Topbar ── */ /* -- Topbar -- */
.topbar{background:var(--bg2);border-bottom:1px solid var(--brd);padding:.75rem 1.5rem;display:flex;align-items:center;gap:1rem;position:sticky;top:0;z-index:100;backdrop-filter:blur(8px)} .topbar{background:var(--bg2);border-bottom:1px solid var(--brd);padding:.75rem 1.5rem;display:flex;align-items:center;gap:1rem;position:sticky;top:0;z-index:100;backdrop-filter:blur(8px)}
.logo{display:flex;align-items:center;gap:.55rem;font-size:1rem;font-weight:700;letter-spacing:-.02em;color:var(--txt)} .logo{display:flex;align-items:center;gap:.55rem;font-size:1rem;font-weight:700;letter-spacing:-.02em;color:var(--txt)}
.logo-dot{width:8px;height:8px;border-radius:50%;background:var(--acc);box-shadow:0 0 8px var(--acc)} .logo-dot{width:8px;height:8px;border-radius:50%;background:var(--acc);box-shadow:0 0 8px var(--acc)}
@@ -1412,7 +1412,7 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
#wifi-label{font-weight:600;color:var(--txt)} #wifi-label{font-weight:600;color:var(--txt)}
#wifi-ip{color:var(--sub);font-family:monospace;font-size:.76rem} #wifi-ip{color:var(--sub);font-family:monospace;font-size:.76rem}
/* ── Layout ── */ /* -- Layout -- */
.page{max-width:1120px;margin:0 auto;padding:1.25rem 1.25rem 0;display:grid;gap:1rem;grid-template-columns:1fr} .page{max-width:1120px;margin:0 auto;padding:1.25rem 1.25rem 0;display:grid;gap:1rem;grid-template-columns:1fr}
@media(min-width:640px){.page{grid-template-columns:1fr 1fr}} @media(min-width:640px){.page{grid-template-columns:1fr 1fr}}
.col2{grid-column:1/-1} .col2{grid-column:1/-1}
@@ -1421,7 +1421,7 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
.site-footer a:hover{color:var(--txt)} .site-footer a:hover{color:var(--txt)}
.site-version{font-family:ui-monospace,monospace;color:var(--brd2)} .site-version{font-family:ui-monospace,monospace;color:var(--brd2)}
/* ── Cards ── */ /* -- Cards -- */
.card{background:var(--surf);border:1px solid var(--brd);border-radius:var(--r2);overflow:hidden} .card{background:var(--surf);border:1px solid var(--brd);border-radius:var(--r2);overflow:hidden}
.card-head{display:flex;align-items:center;gap:.6rem;padding:.75rem 1.1rem;border-bottom:1px solid var(--brd);background:var(--surf2)} .card-head{display:flex;align-items:center;gap:.6rem;padding:.75rem 1.1rem;border-bottom:1px solid var(--brd);background:var(--surf2)}
.card-icon{width:28px;height:28px;border-radius:.45rem;display:flex;align-items:center;justify-content:center;font-size:.95rem;flex-shrink:0} .card-icon{width:28px;height:28px;border-radius:.45rem;display:flex;align-items:center;justify-content:center;font-size:.95rem;flex-shrink:0}
@@ -1434,7 +1434,7 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
.card-sub{font-size:.74rem;color:var(--sub);margin-left:auto} .card-sub{font-size:.74rem;color:var(--sub);margin-left:auto}
.card-body{padding:1.1rem} .card-body{padding:1.1rem}
/* ── Buttons ── */ /* -- Buttons -- */
.btn{display:inline-flex;align-items:center;gap:.35rem;padding:.42rem .9rem;border:1px solid var(--brd2);border-radius:.45rem;background:transparent;color:var(--txt);font-size:.83rem;font-weight:500;cursor:pointer;transition:all .15s;white-space:nowrap;line-height:1.2} .btn{display:inline-flex;align-items:center;gap:.35rem;padding:.42rem .9rem;border:1px solid var(--brd2);border-radius:.45rem;background:transparent;color:var(--txt);font-size:.83rem;font-weight:500;cursor:pointer;transition:all .15s;white-space:nowrap;line-height:1.2}
.btn:hover{border-color:var(--acc);color:var(--acc);background:rgba(79,142,247,.07)} .btn:hover{border-color:var(--acc);color:var(--acc);background:rgba(79,142,247,.07)}
.btn.pri{background:var(--acc);border-color:var(--acc);color:#fff} .btn.pri{background:var(--acc);border-color:var(--acc);color:#fff}
@@ -1449,7 +1449,7 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
.btn:disabled{opacity:.35;cursor:default;pointer-events:none} .btn:disabled{opacity:.35;cursor:default;pointer-events:none}
.btn-row{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:.85rem} .btn-row{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:.85rem}
/* ── Progress ── */ /* -- Progress -- */
.prog-track{height:5px;background:var(--bg);border-radius:9999px;overflow:hidden;margin:.65rem 0 .3rem} .prog-track{height:5px;background:var(--bg);border-radius:9999px;overflow:hidden;margin:.65rem 0 .3rem}
.prog-fill{height:100%;background:linear-gradient(90deg,var(--acc),var(--grn));border-radius:9999px;transition:width .5s ease} .prog-fill{height:100%;background:linear-gradient(90deg,var(--acc),var(--grn));border-radius:9999px;transition:width .5s ease}
.prog-fill.err{background:var(--red)} .prog-fill.err{background:var(--red)}
@@ -1460,11 +1460,11 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
.pill.grn{border-color:rgba(52,211,153,.4);color:var(--grn);background:rgba(52,211,153,.08)} .pill.grn{border-color:rgba(52,211,153,.4);color:var(--grn);background:rgba(52,211,153,.08)}
.pill.red{border-color:rgba(248,113,113,.4);color:var(--red);background:rgba(248,113,113,.08)} .pill.red{border-color:rgba(248,113,113,.4);color:var(--red);background:rgba(248,113,113,.08)}
/* ── Status text ── */ /* -- Status text -- */
.st-headline{font-size:1.05rem;font-weight:700;letter-spacing:-.01em} .st-headline{font-size:1.05rem;font-weight:700;letter-spacing:-.01em}
.st-run{color:var(--acc)}.st-ok{color:var(--grn)}.st-err{color:var(--red)}.st-idle{color:var(--sub)} .st-run{color:var(--acc)}.st-ok{color:var(--grn)}.st-err{color:var(--red)}.st-idle{color:var(--sub)}
/* ── Form fields ── */ /* -- Form fields -- */
.field{margin-bottom:.85rem} .field{margin-bottom:.85rem}
.field:last-child{margin-bottom:0} .field:last-child{margin-bottom:0}
.field label{display:block;font-size:.76rem;font-weight:600;color:var(--sub);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.35rem} .field label{display:block;font-size:.76rem;font-weight:600;color:var(--sub);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.35rem}
@@ -1477,7 +1477,7 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
.flash{font-size:.78rem;min-height:1rem;padding:.2rem 0} .flash{font-size:.78rem;min-height:1rem;padding:.2rem 0}
.flash.ok{color:var(--grn)}.flash.err{color:var(--red)}.flash.warn{color:#f4a332} .flash.ok{color:var(--grn)}.flash.err{color:var(--red)}.flash.warn{color:#f4a332}
/* ── Port Slots ── */ /* -- Port Slots -- */
/* port-pair: immer echtes 1fr 1fr, unabhängig vom Explorer */ /* port-pair: immer echtes 1fr 1fr, unabhängig vom Explorer */
.port-pair{display:grid;grid-template-columns:1fr 1fr;gap:.85rem;align-items:start} .port-pair{display:grid;grid-template-columns:1fr 1fr;gap:.85rem;align-items:start}
@media(max-width:500px){.port-pair{grid-template-columns:1fr}} @media(max-width:500px){.port-pair{grid-template-columns:1fr}}
@@ -1495,13 +1495,13 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
.port-info{font-size:.72rem;color:var(--sub);margin-top:.1rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .port-info{font-size:.72rem;color:var(--sub);margin-top:.1rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.hint-box{font-size:.72rem;color:var(--sub);margin-top:.65rem;padding:.45rem .65rem;background:var(--bg2);border-radius:.4rem;border-left:3px solid var(--brd2);line-height:1.5} .hint-box{font-size:.72rem;color:var(--sub);margin-top:.65rem;padding:.45rem .65rem;background:var(--bg2);border-radius:.4rem;border-left:3px solid var(--brd2);line-height:1.5}
/* ── Port+Explorer grid ── */ /* -- Port+Explorer grid -- */
/* pex-grid: port-pair links, explorer rechts */ /* pex-grid: port-pair links, explorer rechts */
.pex-grid{display:grid;gap:.85rem;grid-template-columns:1fr} .pex-grid{display:grid;gap:.85rem;grid-template-columns:1fr}
@media(min-width:960px){.pex-grid{grid-template-columns:1fr auto}.pex-grid .expl-wrap{width:320px}} @media(min-width:960px){.pex-grid{grid-template-columns:1fr auto}.pex-grid .expl-wrap{width:320px}}
.expl-wrap{border:1px solid var(--brd);border-radius:var(--r);overflow:hidden;display:flex;flex-direction:column} .expl-wrap{border:1px solid var(--brd);border-radius:var(--r);overflow:hidden;display:flex;flex-direction:column}
/* ── File Explorer ── */ /* -- File Explorer -- */
.expl-bar{display:flex;align-items:center;gap:.4rem;padding:.55rem .8rem;background:var(--surf2);border-bottom:1px solid var(--brd);flex-shrink:0} .expl-bar{display:flex;align-items:center;gap:.4rem;padding:.55rem .8rem;background:var(--surf2);border-bottom:1px solid var(--brd);flex-shrink:0}
.etab{padding:.22rem .6rem;border-radius:.35rem;font-size:.76rem;font-weight:600;cursor:pointer;border:1px solid transparent;color:var(--sub);transition:.15s} .etab{padding:.22rem .6rem;border-radius:.35rem;font-size:.76rem;font-weight:600;cursor:pointer;border:1px solid transparent;color:var(--sub);transition:.15s}
.etab.on{background:var(--acc);color:#fff;border-color:var(--acc)} .etab.on{background:var(--acc);color:#fff;border-color:var(--acc)}
@@ -1528,14 +1528,14 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
@media(max-width:360px){.expl-dt{display:none}} @media(max-width:360px){.expl-dt{display:none}}
.expl-empty{padding:1.5rem;text-align:center;color:var(--sub);font-size:.84rem} .expl-empty{padding:1.5rem;text-align:center;color:var(--sub);font-size:.84rem}
/* ── Log ── */ /* -- 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} .log-wrap{font-family:ui-monospace,monospace;font-size:.75rem;max-height:300px;overflow-y:auto;background:var(--bg2);border-radius:.45rem;padding:.5rem}
.log-row{display:flex;gap:.5rem;padding:.18rem 0;border-bottom:1px solid rgba(42,54,80,.5)} .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-row:last-child{border-bottom:none}
.log-t{color:var(--brd2);flex-shrink:0} .log-t{color:var(--brd2);flex-shrink:0}
.log-m{color:var(--sub)} .log-m{color:var(--sub)}
/* ── Upload targets ── */ /* -- Upload targets -- */
.ut-row{display:flex;align-items:center;gap:.55rem;padding:.6rem .75rem;border:1px solid var(--brd);border-radius:.5rem;transition:.15s} .ut-row{display:flex;align-items:center;gap:.55rem;padding:.6rem .75rem;border:1px solid var(--brd);border-radius:.5rem;transition:.15s}
.ut-row.on{border-color:rgba(79,142,247,.35);background:rgba(79,142,247,.04)} .ut-row.on{border-color:rgba(79,142,247,.35);background:rgba(79,142,247,.04)}
.ut-ico{font-size:1.1rem;flex-shrink:0} .ut-ico{font-size:1.1rem;flex-shrink:0}
@@ -1544,7 +1544,7 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
.ut-acts{display:flex;gap:.3rem;margin-left:auto;flex-shrink:0} .ut-acts{display:flex;gap:.3rem;margin-left:auto;flex-shrink:0}
.add-panel{border:1px solid var(--brd);border-radius:.6rem;padding:.9rem;margin-top:.75rem;background:var(--bg2)} .add-panel{border:1px solid var(--brd);border-radius:.6rem;padding:.9rem;margin-top:.75rem;background:var(--bg2)}
/* ── Tabs (WiFi) ── */ /* -- Tabs (WiFi) -- */
.tab-strip{display:flex;gap:.25rem;margin-bottom:.9rem;border-bottom:1px solid var(--brd);padding-bottom:.6rem} .tab-strip{display:flex;gap:.25rem;margin-bottom:.9rem;border-bottom:1px solid var(--brd);padding-bottom:.6rem}
.tab{padding:.28rem .7rem;border-radius:.35rem;font-size:.8rem;font-weight:500;cursor:pointer;color:var(--sub);transition:.15s;border:1px solid transparent} .tab{padding:.28rem .7rem;border-radius:.35rem;font-size:.8rem;font-weight:500;cursor:pointer;color:var(--sub);transition:.15s;border:1px solid transparent}
.tab.on{background:var(--acc);color:#fff;border-color:var(--acc)} .tab.on{background:var(--acc);color:#fff;border-color:var(--acc)}
@@ -1555,12 +1555,12 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
.net-row:hover{border-color:var(--acc);background:rgba(79,142,247,.06)} .net-row:hover{border-color:var(--acc);background:rgba(79,142,247,.06)}
.net-sig{font-size:.7rem;color:var(--sub);margin-left:auto} .net-sig{font-size:.7rem;color:var(--sub);margin-left:auto}
/* ── Divider ── */ /* -- Divider -- */
.sec{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--sub);padding:.1rem 0;margin:.7rem 0 .5rem;display:flex;align-items:center;gap:.5rem} .sec{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--sub);padding:.1rem 0;margin:.7rem 0 .5rem;display:flex;align-items:center;gap:.5rem}
.sec::after{content:'';flex:1;height:1px;background:var(--brd)} .sec::after{content:'';flex:1;height:1px;background:var(--brd)}
.empty{color:var(--sub);font-size:.85rem;padding:.3rem 0} .empty{color:var(--sub);font-size:.85rem;padding:.3rem 0}
/* ── WireGuard VPN ── */ /* -- WireGuard VPN -- */
.wdot.vpn{background:var(--pur);box-shadow:0 0 6px var(--pur)} .wdot.vpn{background:var(--pur);box-shadow:0 0 6px var(--pur)}
</style> </style>
</head> </head>
@@ -1589,7 +1589,7 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
<main class="page"> <main class="page">
<!-- ── Kopierstatus ── --> <!-- -- Kopierstatus -- -->
<div class="card col2"> <div class="card col2">
<div class="card-head"> <div class="card-head">
<div class="card-icon blue">▶</div> <div class="card-icon blue">▶</div>
@@ -1624,15 +1624,15 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
<div class="btn-row"> <div class="btn-row">
<button id="btn-start" class="btn pri" onclick="startCopy()">▶&nbsp;Kopieren starten</button> <button id="btn-start" class="btn pri" onclick="startCopy()">▶&nbsp;Kopieren starten</button>
<button id="btn-cancel" class="btn danger" onclick="cancelCopy()" style="display:none">■&nbsp;Abbrechen</button> <button id="btn-cancel" class="btn danger" onclick="cancelCopy()" style="display:none">■&nbsp;Abbrechen</button>
<button class="btn ghost" onclick="refreshDevices()">&nbsp;Geräte neu laden</button> <button class="btn ghost" onclick="refreshDevices()">->&nbsp;Geräte neu laden</button>
</div> </div>
</div> </div>
</div> </div>
<!-- ── USB Ports + Explorer ── --> <!-- -- USB Ports + Explorer -- -->
<div class="card col2"> <div class="card col2">
<div class="card-head"> <div class="card-head">
<div class="card-icon green"></div> <div class="card-icon green"><-></div>
<span class="card-title">USB Ports &amp; Datei-Explorer</span> <span class="card-title">USB Ports &amp; Datei-Explorer</span>
</div> </div>
<div class="card-body"> <div class="card-body">
@@ -1663,7 +1663,7 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
</div> </div>
<button class="btn grn" style="width:100%" onclick="assignPort('source')">✓&nbsp;Als feste Quelle speichern</button> <button class="btn grn" style="width:100%" onclick="assignPort('source')">✓&nbsp;Als feste Quelle speichern</button>
<div id="src-flash" class="flash" style="margin-top:.4rem"></div> <div id="src-flash" class="flash" style="margin-top:.4rem"></div>
<div class="hint-box">Gerät in den gewünschten Port aus Liste wählen Speichern. PiCopy merkt sich den physischen Port dauerhaft.</div> <div class="hint-box">Gerät in den gewünschten Port -> aus Liste wählen -> Speichern. PiCopy merkt sich den physischen Port dauerhaft.</div>
</div> </div>
<!-- Ziel --> <!-- Ziel -->
@@ -1688,7 +1688,7 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
</div> </div>
<button class="btn pri" style="width:100%" onclick="assignPort('dest')">✓&nbsp;Als festes Ziel speichern</button> <button class="btn pri" style="width:100%" onclick="assignPort('dest')">✓&nbsp;Als festes Ziel speichern</button>
<div id="dst-flash" class="flash" style="margin-top:.4rem"></div> <div id="dst-flash" class="flash" style="margin-top:.4rem"></div>
<div class="hint-box">Gerät in den gewünschten Port aus Liste wählen Speichern. Ab dann wird dieser Port immer als Ziel verwendet.</div> <div class="hint-box">Gerät in den gewünschten Port -> aus Liste wählen -> Speichern. Ab dann wird dieser Port immer als Ziel verwendet.</div>
</div> </div>
</div><!-- /port-pair --> </div><!-- /port-pair -->
@@ -1698,7 +1698,7 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
<div class="expl-bar"> <div class="expl-bar">
<button class="etab on" id="etab-src" onclick="expl.switchRole('src')">⬆ Quelle</button> <button class="etab on" id="etab-src" onclick="expl.switchRole('src')">⬆ Quelle</button>
<button class="etab" id="etab-dst" onclick="expl.switchRole('dst')">⬇ Ziel</button> <button class="etab" id="etab-dst" onclick="expl.switchRole('dst')">⬇ Ziel</button>
<button class="expl-reload" onclick="expl.reload()" title="Neu laden"></button> <button class="expl-reload" onclick="expl.reload()" title="Neu laden">-></button>
</div> </div>
<div class="expl-bread" id="expl-bread"></div> <div class="expl-bread" id="expl-bread"></div>
<div class="expl-scroll" id="expl-body"> <div class="expl-scroll" id="expl-body">
@@ -1716,7 +1716,7 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
</div> </div>
</div> </div>
<!-- ── Kopier-Einstellungen ── --> <!-- -- Kopier-Einstellungen -- -->
<div class="card col2"> <div class="card col2">
<div class="card-head"> <div class="card-head">
<div class="card-icon ylw">⚙</div> <div class="card-icon ylw">⚙</div>
@@ -1789,15 +1789,15 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
</div> </div>
</div> </div>
<!-- ── Upload-Ziele ── --> <!-- -- Upload-Ziele -- -->
<div class="card"> <div class="card">
<div class="card-head"> <div class="card-head">
<div class="card-icon pur"></div> <div class="card-icon pur">^</div>
<span class="card-title">Fernkopie - NAS / SMB</span> <span class="card-title">Fernkopie - NAS / SMB</span>
</div> </div>
<div class="card-body"> <div class="card-body">
<div id="ut-list" style="display:flex;flex-direction:column;gap:.45rem;margin-bottom:.65rem"></div> <div id="ut-list" style="display:flex;flex-direction:column;gap:.45rem;margin-bottom:.65rem"></div>
<button class="btn" onclick="utToggleForm()" id="ut-add-btn">&nbsp;NAS-Ziel hinzufügen</button> <button class="btn" onclick="utToggleForm()" id="ut-add-btn">+&nbsp;NAS-Ziel hinzufügen</button>
<div id="ut-form" class="add-panel" style="display:none"> <div id="ut-form" class="add-panel" style="display:none">
<div class="sec" style="margin-top:0">SMB / Netzlaufwerk</div> <div class="sec" style="margin-top:0">SMB / Netzlaufwerk</div>
@@ -1818,7 +1818,7 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
</div> </div>
</div> </div>
<!-- ── WiFi + Log nebeneinander ── --> <!-- -- WiFi + Log nebeneinander -- -->
<div class="card"> <div class="card">
<div class="card-head"> <div class="card-head">
<div class="card-icon acc" style="background:rgba(79,142,247,.15);color:var(--acc)">⌘</div> <div class="card-icon acc" style="background:rgba(79,142,247,.15);color:var(--acc)">⌘</div>
@@ -1853,7 +1853,7 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
</div> </div>
</div> </div>
<!-- ── WireGuard VPN ── --> <!-- -- WireGuard VPN -- -->
<div class="card"> <div class="card">
<div class="card-head"> <div class="card-head">
<div class="card-icon pur">⚿</div> <div class="card-icon pur">⚿</div>
@@ -1919,7 +1919,7 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
</div> </div>
</div> </div>
<!-- ── System ── --> <!-- -- System -- -->
<div class="card"> <div class="card">
<div class="card-head"> <div class="card-head">
<div class="card-icon" style="background:rgba(255,180,60,.1);color:#f4a332">⚙</div> <div class="card-icon" style="background:rgba(255,180,60,.1);color:#f4a332">⚙</div>
@@ -1928,14 +1928,14 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
<div class="card-body" style="display:flex;flex-direction:column;gap:.6rem"> <div class="card-body" style="display:flex;flex-direction:column;gap:.6rem">
<button class="btn" style="width:100%" onclick="checkUpdate()">🔍&nbsp;Nach Update suchen</button> <button class="btn" style="width:100%" onclick="checkUpdate()">🔍&nbsp;Nach Update suchen</button>
<div id="sys-update-flash" class="flash" style="display:none"></div> <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()">&nbsp;Gerät neu starten</button> <button class="btn" style="width:100%;background:rgba(220,60,60,.12);color:#e05555;border-color:rgba(220,60,60,.25)" onclick="rebootDevice()"><-&nbsp;Gerät neu starten</button>
</div> </div>
</div> </div>
<!-- ── Logs ── --> <!-- -- Logs -- -->
<div class="card col2"> <div class="card col2">
<div class="card-head"> <div class="card-head">
<div class="card-icon" style="background:rgba(139,154,181,.1);color:var(--sub)"></div> <div class="card-icon" style="background:rgba(139,154,181,.1);color:var(--sub)">=</div>
<span class="card-title">Logs</span> <span class="card-title">Logs</span>
</div> </div>
<div class="card-body" style="padding:.65rem .85rem"> <div class="card-body" style="padding:.65rem .85rem">
@@ -1957,7 +1957,7 @@ const api = async (p, m='GET', b=null) => {
return (await fetch('/api'+p,o)).json(); return (await fetch('/api'+p,o)).json();
}; };
// ── Tabs ───────────────────────────────────────────────────────────────────── // -- Tabs ---------------------------------------------------------------------
function swTab(show,hide){ function swTab(show,hide){
$(show).classList.add('on'); $(hide).classList.remove('on'); $(show).classList.add('on'); $(hide).classList.remove('on');
document.querySelectorAll('.tab').forEach(t=> document.querySelectorAll('.tab').forEach(t=>
@@ -1965,7 +1965,7 @@ function swTab(show,hide){
); );
} }
// ── Port Slots ──────────────────────────────────────────────────────────────── // -- Port Slots ----------------------------------------------------------------
async function refreshDevices(){ async function refreshDevices(){
devs = await api('/devices'); devs = await api('/devices');
renderSlot('src', cfg.source_port, cfg.source_label); renderSlot('src', cfg.source_port, cfg.source_label);
@@ -2030,7 +2030,7 @@ async function assignPort(role){
const el=$(id); if(el) el.addEventListener('input',()=>el.dataset.dirty='1'); const el=$(id); if(el) el.addEventListener('input',()=>el.dataset.dirty='1');
})); }));
// ── Copy ────────────────────────────────────────────────────────────────────── // -- Copy ----------------------------------------------------------------------
async function startCopy(){ async function startCopy(){
_dismissed=false; _dismissed=false;
if(_autoDismissTimer){ clearTimeout(_autoDismissTimer); _autoDismissTimer=null; } if(_autoDismissTimer){ clearTimeout(_autoDismissTimer); _autoDismissTimer=null; }
@@ -2038,7 +2038,7 @@ async function startCopy(){
} }
async function cancelCopy(){ await api('/copy/cancel','POST'); } async function cancelCopy(){ await api('/copy/cancel','POST'); }
// ── Config ──────────────────────────────────────────────────────────────────── // -- Config --------------------------------------------------------------------
async function loadCfg(){ async function loadCfg(){
cfg=await api('/config'); cfg=await api('/config');
$('c-fmt').value=cfg.folder_format||'%Y-%m-%d'; $('c-fmt').value=cfg.folder_format||'%Y-%m-%d';
@@ -2064,7 +2064,7 @@ async function saveCopyCfg(){
} }
function setFilter(v){ $('c-filter').value=v; } function setFilter(v){ $('c-filter').value=v; }
// ── WiFi ────────────────────────────────────────────────────────────────────── // -- WiFi ----------------------------------------------------------------------
async function scanNets(){ async function scanNets(){
$('net-list').style.display='flex'; $('net-list').innerHTML='<div class="expl-empty" style="padding:.5rem">Suche...</div>'; $('net-list').style.display='flex'; $('net-list').innerHTML='<div class="expl-empty" style="padding:.5rem">Suche...</div>';
const nets=await api('/wifi/scan'); const nets=await api('/wifi/scan');
@@ -2092,7 +2092,7 @@ async function saveAP(){
else flash('ap-flash','ok','Gespeichert! Hotspot startet neu.'); else flash('ap-flash','ok','Gespeichert! Hotspot startet neu.');
} }
// ── Upload-Ziele ────────────────────────────────────────────────────────────── // -- Upload-Ziele --------------------------------------------------------------
const UT_ICONS={smb:'🖧',onedrive:'',drive:'📄',dropbox:'📦'}; const UT_ICONS={smb:'🖧',onedrive:'',drive:'📄',dropbox:'📦'};
const UT_LABELS={smb:'SMB/NAS',onedrive:'OneDrive',drive:'Google Drive',dropbox:'Dropbox'}; const UT_LABELS={smb:'SMB/NAS',onedrive:'OneDrive',drive:'Google Drive',dropbox:'Dropbox'};
const UT_CMD={onedrive:'rclone authorize "onedrive"',drive:'rclone authorize "drive"',dropbox:'rclone authorize "dropbox"'}; const UT_CMD={onedrive:'rclone authorize "onedrive"',drive:'rclone authorize "drive"',dropbox:'rclone authorize "dropbox"'};
@@ -2119,7 +2119,7 @@ function renderUTs(){
function utToggleForm(){ function utToggleForm(){
const f=$('ut-form'),b=$('ut-add-btn'),show=f.style.display==='none'; const f=$('ut-form'),b=$('ut-add-btn'),show=f.style.display==='none';
f.style.display=show?'block':'none'; f.style.display=show?'block':'none';
b.innerHTML=show?'✕ Abbrechen':' Ziel hinzufügen'; b.innerHTML=show?'✕ Abbrechen':'+ Ziel hinzufügen';
if(show){utSelectType('smb',document.querySelector('.sel-opt'));} if(show){utSelectType('smb',document.querySelector('.sel-opt'));}
} }
function utSelectType(type,el){ function utSelectType(type,el){
@@ -2164,7 +2164,7 @@ async function utDel(id,name){
await api('/upload/targets/'+id,'DELETE');await loadUTs(); await api('/upload/targets/'+id,'DELETE');await loadUTs();
} }
// ── File Explorer ───────────────────────────────────────────────────────────── // -- File Explorer -------------------------------------------------------------
const expl={ const expl={
role:'src', paths:{src:'',dst:''}, role:'src', paths:{src:'',dst:''},
switchRole(r){ switchRole(r){
@@ -2197,7 +2197,7 @@ const expl={
let acc=''; let acc='';
path.split('/').filter(Boolean).forEach(p=>{ path.split('/').filter(Boolean).forEach(p=>{
acc+=(acc?'/':'')+p;const a=acc; acc+=(acc?'/':'')+p;const a=acc;
h+=`<span class="bsep"> </span><span class="bseg" onclick="expl.navigate('${a.replace(/'/g,"\\'")}')">${p}</span>`; h+=`<span class="bsep"> > </span><span class="bseg" onclick="expl.navigate('${a.replace(/'/g,"\\'")}')">${p}</span>`;
}); });
} }
el.innerHTML=h; el.innerHTML=h;
@@ -2207,7 +2207,7 @@ const expl={
let h=''; let h='';
if(cur){ if(cur){
const par=cur.includes('/')?cur.substring(0,cur.lastIndexOf('/')):''; const par=cur.includes('/')?cur.substring(0,cur.lastIndexOf('/')):'';
h+=`<div class="expl-row up" onclick="expl.navigate('${par}')"><span class="expl-ico"></span><span class="expl-nm" style="color:var(--sub)">..</span><span></span><span></span></div>`; h+=`<div class="expl-row up" onclick="expl.navigate('${par}')"><span class="expl-ico"><-</span><span class="expl-nm" style="color:var(--sub)">..</span><span></span><span></span></div>`;
} }
if(!entries.length&&!cur){body.innerHTML='<div class="expl-empty">Laufwerk leer</div>';return;} if(!entries.length&&!cur){body.innerHTML='<div class="expl-empty">Laufwerk leer</div>';return;}
if(!entries.length){body.innerHTML=h+'<div class="expl-empty">Ordner leer</div>';return;} if(!entries.length){body.innerHTML=h+'<div class="expl-empty">Ordner leer</div>';return;}
@@ -2255,7 +2255,7 @@ function fmtSpd(bps){
return(bps/1048576).toFixed(1)+' MB/s'; return(bps/1048576).toFixed(1)+' MB/s';
} }
// ── Poll ────────────────────────────────────────────────────────────────────── // -- Poll ----------------------------------------------------------------------
async function poll(){ async function poll(){
try{ try{
const {copy:c,wifi:w,vpn:v}=await api('/status'); const {copy:c,wifi:w,vpn:v}=await api('/status');
@@ -2382,7 +2382,7 @@ function dismissStatus(){
$('st-dismiss').style.display='none'; $('st-dismiss').style.display='none';
} }
// ── Update ──────────────────────────────────────────────────────────────────── // -- Update --------------------------------------------------------------------
async function pollUpdate() { async function pollUpdate() {
try { try {
const u = await api('/update/status'); const u = await api('/update/status');
@@ -2405,7 +2405,7 @@ async function installUpdate() {
'Das Web-Interface ist für ca. 10 Sekunden nicht erreichbar.' 'Das Web-Interface ist für ca. 10 Sekunden nicht erreichbar.'
)) return; )) return;
$('upd-badge').innerHTML = ' Installiere...'; $('upd-badge').innerHTML = '-> Installiere...';
$('upd-badge').style.pointerEvents = 'none'; $('upd-badge').style.pointerEvents = 'none';
try { try {
@@ -2425,7 +2425,7 @@ async function installUpdate() {
async function checkUpdate() { async function checkUpdate() {
const btn = event.currentTarget; const btn = event.currentTarget;
btn.disabled = true; btn.textContent = ' Prüfe...'; btn.disabled = true; btn.textContent = '-> Prüfe...';
try { try {
await api('/update/check', 'POST'); await api('/update/check', 'POST');
// Warten bis der Server-Check abgeschlossen ist (max 15 s, alle 500 ms) // Warten bis der Server-Check abgeschlossen ist (max 15 s, alle 500 ms)
@@ -2457,14 +2457,14 @@ async function checkUpdate() {
async function rebootDevice() { async function rebootDevice() {
if (!confirm('Gerät jetzt neu starten?\n\nDas Web-Interface ist für ca. 30 Sekunden nicht erreichbar.')) return; if (!confirm('Gerät jetzt neu starten?\n\nDas Web-Interface ist für ca. 30 Sekunden nicht erreichbar.')) return;
try { await api('/system/reboot', 'POST'); } catch(e) {} try { await api('/system/reboot', 'POST'); } catch(e) {}
document.body.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;color:#888;font-size:1rem"> Gerät startet neu - bitte warten...</div>'; document.body.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;color:#888;font-size:1rem">-> Gerät startet neu - bitte warten...</div>';
setTimeout(async function waitForRestart() { setTimeout(async function waitForRestart() {
try { await fetch('/api/update/status'); location.reload(); } try { await fetch('/api/update/status'); location.reload(); }
catch(e) { setTimeout(waitForRestart, 2000); } catch(e) { setTimeout(waitForRestart, 2000); }
}, 10000); }, 10000);
} }
// ── WireGuard VPN ───────────────────────────────────────────────────────────── // -- WireGuard VPN -------------------------------------------------------------
async function wgInstall(){ async function wgInstall(){
if(!confirm('wireguard + wireguard-tools jetzt per apt-get installieren?\n\nDauer: ca. 30-90 Sekunden.'))return; if(!confirm('wireguard + wireguard-tools jetzt per apt-get installieren?\n\nDauer: ca. 30-90 Sekunden.'))return;
flash('wg-flash','ok','Starte Installation...'); flash('wg-flash','ok','Starte Installation...');

View File

@@ -1 +1 @@
1.0.19 1.0.20