diff --git a/app.py b/app.py index 419f4d8..7786617 100644 --- a/app.py +++ b/app.py @@ -521,7 +521,71 @@ def r_wifi_status(): with wifi_lock: return jsonify(dict(wifi_state)) -# ── HTML Template ───────────────────────────────────────────────────────────── + +# ── Browse (persistente Mounts für File-Explorer) ───────────────────────────── + +_browse_mounts = {} # usb_port -> mount_point + +def get_browse_mp(dev): + port = dev.get('usb_port', '') + if dev.get('mount'): + return dev['mount'] + mp = _browse_mounts.get(port) + if mp and Path(mp).is_dir(): + return mp + mp = f'/mnt/picopy_br_{port}' + os.makedirs(mp, exist_ok=True) + r = subprocess.run(['mount', dev['device'], mp], capture_output=True) + if r.returncode == 0: + _browse_mounts[port] = mp + return mp + return None + + +@app.route('/api/browse') +def r_browse(): + port = request.args.get('port', '') + rpath = request.args.get('path', '').lstrip('/') + + devs = usb_devices() + dev = next((d for d in devs if d['usb_port'] == port), None) + if not dev: + return jsonify(error='Gerät nicht verbunden'), 404 + + mp = get_browse_mp(dev) + if not mp: + return jsonify(error='Gerät nicht mountbar'), 500 + + try: + base = Path(mp).resolve() + target = (base / rpath).resolve() + + if not str(target).startswith(str(base)): + return jsonify(error='Ungültiger Pfad'), 400 + if not target.is_dir(): + return jsonify(error='Kein Verzeichnis'), 400 + + entries = [] + for item in sorted(target.iterdir(), + key=lambda x: (x.is_file(), x.name.lower())): + try: + s = item.stat() + entries.append({ + 'name': item.name, + 'dir': item.is_dir(), + 'size': s.st_size if item.is_file() else None, + 'mtime': datetime.fromtimestamp(s.st_mtime).strftime('%d.%m.%y %H:%M'), + }) + except Exception: + pass + + rel = str(target.relative_to(base)) + return jsonify(path='' if rel == '.' else rel, entries=entries) + + except Exception as e: + return jsonify(error=str(e)), 500 + + # ── HTML Template ───────────────────────────────────────────────────────────── @@ -537,7 +601,7 @@ HTML = r""" body{background:var(--bg);color:var(--txt);font-family:system-ui,sans-serif;padding:1rem 1rem 4rem;min-height:100vh} h1{font-size:1.35rem;font-weight:700;display:flex;align-items:center;gap:.5rem;margin-bottom:1.25rem} h2{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--mut);margin-bottom:.8rem} -.wrap{max-width:960px;margin:0 auto;display:grid;gap:.85rem;grid-template-columns:1fr} +.wrap{max-width:1100px;margin:0 auto;display:grid;gap:.85rem;grid-template-columns:1fr} @media(min-width:600px){.wrap{grid-template-columns:1fr 1fr}} .card{background:var(--s1);border:1px solid var(--brd);border-radius:.8rem;padding:1.1rem} .span2{grid-column:1/-1} @@ -560,7 +624,6 @@ h2{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08e .field label{display:block;font-size:.81rem;color:var(--mut);margin-bottom:.3rem} .field input,.field select{width:100%;padding:.44rem .65rem;background:var(--bg);border:1px solid var(--brd);border-radius:.4rem;color:var(--txt);font-size:.88rem} .field input:focus,.field select:focus{outline:none;border-color:var(--acc)} -.field input[type=password]{letter-spacing:.05em} .tog{display:flex;align-items:center;gap:.5rem;margin-bottom:.65rem;cursor:pointer;user-select:none;font-size:.88rem} .tog input{accent-color:var(--acc);width:16px;height:16px;cursor:pointer} .log-box{font-family:ui-monospace,monospace;font-size:.76rem;max-height:220px;overflow-y:auto} @@ -581,9 +644,8 @@ h2{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08e .net-signal{font-size:.72rem;color:var(--mut);margin-left:auto} .flash{font-size:.78rem;padding:.25rem 0;min-height:1.2rem} .flash.ok{color:var(--grn)}.flash.err{color:var(--red)} -/* Port Slots */ -.port-grid{display:grid;grid-template-columns:1fr 1fr;gap:.85rem} -@media(max-width:599px){.port-grid{grid-template-columns:1fr}} + +/* ── Port Slots ── */ .port-slot{border:2px solid var(--brd);border-radius:.7rem;padding:1rem;transition:border-color .2s} .port-slot.has-src{border-color:var(--grn)} .port-slot.has-dst{border-color:var(--acc)} @@ -597,6 +659,42 @@ h2{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08e .port-dev-name{font-weight:600;font-size:.9rem;line-height:1.3} .port-dev-sub{font-size:.73rem;color:var(--mut);font-family:monospace;margin-top:.1rem} .port-hint{font-size:.73rem;color:var(--mut);margin-top:.65rem;padding:.5rem .65rem;background:var(--bg);border-radius:.4rem;border-left:3px solid var(--brd)} + +/* ── Port + Explorer grid ── */ +.port-and-expl{display:grid;gap:.85rem;grid-template-columns:1fr 1fr} +@media(max-width:599px){.port-and-expl{grid-template-columns:1fr}} +@media(min-width:900px){.port-and-expl{grid-template-columns:1fr 1fr 1.3fr}} +.expl-col{border:1px solid var(--brd);border-radius:.7rem;overflow:hidden;display:flex;flex-direction:column} +@media(max-width:899px){.expl-col{grid-column:1/-1}} +@media(min-width:900px){.expl-col{grid-column:3;grid-row:1}} + +/* ── File Explorer ── */ +.expl-header{padding:.6rem .8rem;background:var(--s2);border-bottom:1px solid var(--brd);display:flex;align-items:center;gap:.4rem;flex-shrink:0} +.expl-tab-btn{padding:.25rem .65rem;border-radius:.35rem;font-size:.78rem;cursor:pointer;border:1px solid var(--brd);background:transparent;color:var(--mut);transition:.15s} +.expl-tab-btn.active{background:var(--acc);border-color:var(--acc);color:#fff} +.expl-tab-btn:hover:not(.active){border-color:var(--acc);color:var(--acc)} +.expl-refresh{margin-left:auto;background:transparent;border:1px solid var(--brd);color:var(--mut);border-radius:.35rem;padding:.22rem .5rem;cursor:pointer;font-size:.85rem;transition:.15s} +.expl-refresh:hover{border-color:var(--acc);color:var(--acc)} +.expl-bread{padding:.45rem .8rem;font-size:.76rem;background:var(--bg);border-bottom:1px solid var(--brd);color:var(--mut);display:flex;align-items:center;gap:.25rem;flex-wrap:wrap;flex-shrink:0;min-height:32px} +.bread-seg{cursor:pointer;color:var(--acc);transition:.1s} +.bread-seg:hover{text-decoration:underline} +.bread-sep{color:var(--brd)} +.expl-body{flex:1;overflow-y:auto;max-height:380px} +@media(min-width:900px){.expl-body{max-height:420px}} +.expl-empty{padding:1.2rem .8rem;color:var(--mut);font-size:.84rem;text-align:center} +.expl-item{display:grid;grid-template-columns:1.6rem 1fr auto auto;align-items:center;gap:.35rem;padding:.38rem .75rem;border-bottom:1px solid rgba(51,65,85,.4);font-size:.82rem;transition:background .1s;cursor:default} +.expl-item:last-child{border-bottom:none} +.expl-item.is-dir{cursor:pointer} +.expl-item.is-dir:hover{background:rgba(59,130,246,.07)} +.expl-item.is-up{cursor:pointer;color:var(--mut)} +.expl-item.is-up:hover{background:rgba(148,163,184,.07)} +.expl-icon{font-size:1rem;text-align:center;line-height:1} +.expl-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0} +.expl-item.is-dir .expl-name{font-weight:500} +.expl-size{font-size:.72rem;color:var(--mut);text-align:right;white-space:nowrap;font-family:monospace} +.expl-date{font-size:.7rem;color:var(--brd);white-space:nowrap;margin-left:.2rem} +@media(max-width:400px){.expl-date{display:none}} +.expl-arrow{color:var(--brd);font-size:.7rem} @@ -633,11 +731,11 @@ h2{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08e - +
-

USB Port Konfiguration

+

USB Port Konfiguration & Datei-Explorer

-
+
@@ -661,7 +759,7 @@ h2{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08e
-
Stecke den USB-Stick in den gewünschten Port, wähle ihn hier aus und klicke Speichern. PiCopy merkt sich diesen physischen Port dauerhaft.
+
Gerät einstecken → aus Liste wählen → Speichern. PiCopy merkt sich diesen physischen Port dauerhaft.
@@ -686,12 +784,27 @@ h2{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08e
-
Stecke das Ziel-Laufwerk in den gewünschten Port, wähle es aus und klicke Speichern. Ab dann wird dieser Port immer als Ziel verwendet.
+
Gerät einstecken → aus Liste wählen → Speichern. Ab dann wird dieser Port immer als Ziel verwendet.
+
+ + +
+
+ + + +
+
+ +
+
+
Gerät verbinden und Port konfigurieren
+
- + -
- - -
+
-
Dieser Hotspot startet automatisch wenn kein Heimnetz erreichbar ist.
IP des Pi im Hotspot-Modus: 10.42.0.1:8080
-
- - -
-
- - -
+
Startet automatisch wenn kein Heimnetz erreichbar ist.
IP im Hotspot-Modus: 10.42.0.1:8080
+
+
@@ -774,9 +878,9 @@ const api = async (path, method='GET', body=null) => { // ── Tabs ────────────────────────────────────────────────────────────────── function switchTab(show, hide) { - $(show).classList.add('active'); $(hide).classList.remove('active'); + $(show).classList.add('active'); $(hide).classList.remove('active'); document.querySelectorAll('.tab').forEach(t => - t.classList.toggle('active', t.textContent.trim() === (show==='tab-client' ? 'Heimnetz' : 'Hotspot (AP)')) + t.classList.toggle('active', t.textContent.trim().startsWith(show==='tab-client'?'Heim':'Hot')) ); } @@ -796,29 +900,23 @@ function renderPortSlots() { function renderSlot(role, port, label) { const isSrc = role === 'src'; const dev = devs.find(d => d.usb_port === port); - const dot = $(role+'-dot'); - const nameEl = $(role+'-dev-name'); - const subEl = $(role+'-dev-sub'); - const slotEl = $('slot-'+role); - const lblEl = $(role+'-label'); - + const dot = $(role+'-dot'), nameEl=$(role+'-dev-name'), subEl=$(role+'-dev-sub'); + const slotEl = $('slot-'+role), lblEl=$(role+'-label'); slotEl.classList.toggle('has-src', isSrc && !!port); slotEl.classList.toggle('has-dst', !isSrc && !!port); - if (dev) { dot.className = 'pdot on'; nameEl.textContent = dev.label || dev.device; - subEl.textContent = 'Port ' + port + (dev.size ? ' · ' + dev.size : '') + (dev.mount ? ' · ' + dev.mount : ''); + subEl.textContent = 'Port '+port+(dev.size?' · '+dev.size:'')+(dev.mount?' · '+dev.mount:''); } else if (port) { dot.className = 'pdot off'; nameEl.textContent = label || 'Nicht verbunden'; - subEl.textContent = 'Konfigurierter Port: ' + port + (label ? ' · ' + label : ''); + subEl.textContent = 'Konfiguriert: Port '+port+(label?' · '+label:''); } else { dot.className = 'pdot off'; nameEl.textContent = 'Nicht verbunden'; subEl.textContent = 'Kein Port konfiguriert'; } - if (lblEl && !lblEl.dataset.dirty) lblEl.value = label || ''; } @@ -827,18 +925,18 @@ function populateSelects() { `` ).join(''); ['src-select','dst-select'].forEach(id => { - const el = $(id), prev = el.value; - el.innerHTML = '' + opts; - if (prev && devs.find(d => d.usb_port === prev)) el.value = prev; + const el=$(id), prev=el.value; + el.innerHTML=''+opts; + if (prev && devs.find(d => d.usb_port===prev)) el.value=prev; }); } function renderUnassigned() { - const list = devs.filter(d => d.usb_port !== cfg.source_port && d.usb_port !== cfg.dest_port); + const list = devs.filter(d => d.usb_port!==cfg.source_port && d.usb_port!==cfg.dest_port); const wrap = $('unassigned-wrap'); - if (!list.length) { wrap.style.display = 'none'; return; } - wrap.style.display = 'block'; - $('unassigned-list').innerHTML = list.map(d => ` + if (!list.length) { wrap.style.display='none'; return; } + wrap.style.display='block'; + $('unassigned-list').innerHTML = list.map(d=>`
${d.label||d.device} @@ -846,159 +944,214 @@ function renderUnassigned() {
`).join(''); } -// ── Assign port ─────────────────────────────────────────────────────────── async function assignPort(role) { - const isSrc = role === 'source'; - const selId = isSrc ? 'src-select' : 'dst-select'; - const lblId = isSrc ? 'src-label' : 'dst-label'; - const flashId = isSrc ? 'src-flash' : 'dst-flash'; - const portKey = isSrc ? 'source_port' : 'dest_port'; - const labelKey = isSrc ? 'source_label': 'dest_label'; - const port = $(selId).value; - const label = $(lblId).value.trim(); - - if (!port) { flash(flashId,'err','Bitte zuerst ein Gerät aus der Liste wählen.'); return; } - - const otherPort = isSrc ? cfg.dest_port : cfg.source_port; - if (port === otherPort) { - flash(flashId,'err','Dieser Port ist bereits als '+(isSrc?'Ziel':'Quelle')+' konfiguriert!'); return; - } - - cfg[portKey] = port; - cfg[labelKey] = label; - $(lblId).dataset.dirty = ''; + const isSrc=role==='source', selId=isSrc?'src-select':'dst-select'; + const lblId=isSrc?'src-label':'dst-label', fId=isSrc?'src-flash':'dst-flash'; + const port=$(selId).value, label=$(lblId).value.trim(); + if (!port) { flash(fId,'err','Bitte zuerst ein Gerät wählen.'); return; } + const other = isSrc ? cfg.dest_port : cfg.source_port; + if (port===other) { flash(fId,'err','Dieser Port ist bereits als '+(isSrc?'Ziel':'Quelle')+' konfiguriert!'); return; } + cfg[isSrc?'source_port':'dest_port'] = port; + cfg[isSrc?'source_label':'dest_label'] = label; + $(lblId).dataset.dirty=''; await api('/config','POST',cfg); - flash(flashId,'ok','Gespeichert — Port '+port+' ist jetzt feste '+(isSrc?'Quelle':'Ziel')+'.'); - renderPortSlots(); - renderUnassigned(); + flash(fId,'ok','Gespeichert — Port '+port+' ist jetzt feste '+(isSrc?'Quelle':'Ziel')+'.'); + renderPortSlots(); renderUnassigned(); + expl.reload(); } -['src-label','dst-label'].forEach(id => { - window.addEventListener('DOMContentLoaded', () => { - const el = $(id); - if (el) el.addEventListener('input', () => { el.dataset.dirty = '1'; }); - }); -}); +['src-label','dst-label'].forEach(id => + window.addEventListener('DOMContentLoaded',()=>{ + const el=$(id); if(el) el.addEventListener('input',()=>el.dataset.dirty='1'); + }) +); // ── Copy ────────────────────────────────────────────────────────────────── -async function startCopy() { - const r = await api('/copy/start','POST'); - if (r.error) alert('Fehler: '+r.error); -} +async function startCopy() { const r=await api('/copy/start','POST'); if(r.error) alert('Fehler: '+r.error); } async function cancelCopy() { await api('/copy/cancel','POST'); } // ── Settings ────────────────────────────────────────────────────────────── async function loadCfg() { - cfg = await api('/config'); - $('c-fmt').value = cfg.folder_format || '%Y-%m-%d'; - $('c-time').checked = !!cfg.add_time; - $('c-sub').checked = !!cfg.subfolder; - $('c-auto').checked = !!cfg.auto_copy; - $('w-ssid').value = cfg.wifi_ssid || ''; - $('ap-ssid').value = cfg.ap_ssid || 'PiCopy'; + cfg=await api('/config'); + $('c-fmt').value=$('c-fmt').querySelector(`[value="${cfg.folder_format||'%Y-%m-%d'}"]`)?.value||'%Y-%m-%d'; + $('c-fmt').value=cfg.folder_format||'%Y-%m-%d'; + $('c-time').checked=!!cfg.add_time; $('c-sub').checked=!!cfg.subfolder; $('c-auto').checked=!!cfg.auto_copy; + $('w-ssid').value=cfg.wifi_ssid||''; $('ap-ssid').value=cfg.ap_ssid||'PiCopy'; } - async function saveCopyCfg() { - cfg.folder_format = $('c-fmt').value; - cfg.add_time = $('c-time').checked; - cfg.subfolder = $('c-sub').checked; - cfg.auto_copy = $('c-auto').checked; - await api('/config','POST',cfg); - flash('copy-cfg-msg','ok','Gespeichert!'); + cfg.folder_format=$('c-fmt').value; cfg.add_time=$('c-time').checked; + cfg.subfolder=$('c-sub').checked; cfg.auto_copy=$('c-auto').checked; + await api('/config','POST',cfg); flash('copy-cfg-msg','ok','Gespeichert!'); } // ── WiFi ────────────────────────────────────────────────────────────────── async function scanNetworks() { - $('net-list').style.display='flex'; - $('net-list').innerHTML='
Suche Netzwerke…
'; - const nets = await api('/wifi/scan'); - if (!nets.length) { $('net-list').innerHTML='
Keine Netzwerke gefunden
'; return; } - $('net-list').innerHTML = nets.map(n => { - const b = n.signal>66?'▂▄▆█':n.signal>33?'▂▄▆░':'▂▄░░'; - return `
${n.ssid}${b} ${n.signal}%
`; + $('net-list').style.display='flex'; $('net-list').innerHTML='
Suche…
'; + const nets=await api('/wifi/scan'); + if(!nets.length){$('net-list').innerHTML='
Keine Netzwerke gefunden
';return;} + $('net-list').innerHTML=nets.map(n=>{ + const b=n.signal>66?'▂▄▆█':n.signal>33?'▂▄▆░':'▂▄░░'; + return`
${n.ssid}${b} ${n.signal}%
`; }).join(''); } -function selectNet(ssid) { $('w-ssid').value=ssid; $('net-list').style.display='none'; $('w-pw').focus(); } - -async function connectWifi() { - const ssid=$('w-ssid').value.trim(), pw=$('w-pw').value; - if (!ssid) { flash('wifi-flash','err','Bitte SSID eingeben'); return; } +function selectNet(ssid){$('w-ssid').value=ssid;$('net-list').style.display='none';$('w-pw').focus();} +async function connectWifi(){ + const ssid=$('w-ssid').value.trim(),pw=$('w-pw').value; + if(!ssid){flash('wifi-flash','err','Bitte SSID eingeben');return;} flash('wifi-flash','ok','Verbinde… (kann 30s dauern)'); - const r = await api('/wifi/connect','POST',{ssid,password:pw}); - if (r.error) flash('wifi-flash','err',r.error); - else flash('wifi-flash','ok','Gestartet. Bei Erfolg erscheint oben die neue IP.'); + const r=await api('/wifi/connect','POST',{ssid,password:pw}); + if(r.error)flash('wifi-flash','err',r.error);else flash('wifi-flash','ok','Gestartet. Bei Erfolg erscheint oben die neue IP.'); +} +async function saveAP(){ + const ssid=$('ap-ssid').value.trim(),pw=$('ap-pw').value; + if(!ssid){flash('ap-flash','err','SSID fehlt');return;} + if(pw.length<8){flash('ap-flash','err','Passwort min. 8 Zeichen');return;} + const r=await api('/wifi/ap','POST',{ssid,password:pw}); + if(r.error)flash('ap-flash','err',r.error);else flash('ap-flash','ok','Gespeichert! Hotspot wird neu gestartet.'); } -async function saveAP() { - const ssid=$('ap-ssid').value.trim(), pw=$('ap-pw').value; - if (!ssid) { flash('ap-flash','err','SSID fehlt'); return; } - if (pw.length<8) { flash('ap-flash','err','Passwort min. 8 Zeichen'); return; } - const r = await api('/wifi/ap','POST',{ssid,password:pw}); - if (r.error) flash('ap-flash','err',r.error); - else flash('ap-flash','ok','Gespeichert! Hotspot wird neu gestartet.'); +// ── File Explorer ───────────────────────────────────────────────────────── +const expl = { + role: 'src', + paths: {src:'', dst:''}, + + switchRole(role) { + this.role = role; + $('expl-tab-src').classList.toggle('active', role==='src'); + $('expl-tab-dst').classList.toggle('active', role==='dst'); + this.load(this.paths[role]); + }, + + reload() { this.load(this.paths[this.role]); }, + + navigate(path) { this.load(path); }, + + async load(path='') { + const port = this.role==='src' ? cfg.source_port : cfg.dest_port; + const body = $('expl-body'), bread = $('expl-bread'); + + if (!port) { + body.innerHTML='
Kein Port konfiguriert
'; + bread.innerHTML=''; + return; + } + const dev = devs.find(d=>d.usb_port===port); + if (!dev) { + body.innerHTML='
Gerät nicht verbunden
'; + bread.innerHTML=''; + return; + } + + body.innerHTML='
Lade…
'; + try { + const data = await api(`/browse?port=${encodeURIComponent(port)}&path=${encodeURIComponent(path)}`); + if (data.error) { body.innerHTML=`
⚠ ${data.error}
`; return; } + this.paths[this.role] = data.path || ''; + this._renderBread(data.path||'', dev.label||dev.device); + this._renderList(data.entries||[], data.path||''); + } catch(e) { + body.innerHTML='
Verbindungsfehler
'; + } + }, + + _renderBread(path, devLabel) { + const bread=$('expl-bread'); + let html=`⌂ ${devLabel}`; + if (path) { + const parts=path.split('/').filter(Boolean); + let acc=''; + parts.forEach(p=>{ + acc+=(acc?'/':'')+p; + const a=acc; + html+=`${p}`; + }); + } + bread.innerHTML=html; + }, + + _renderList(entries, curPath) { + const body=$('expl-body'); + if (!entries.length && !curPath) { body.innerHTML='
Laufwerk ist leer
'; return; } + + let html=''; + if (curPath) { + const parent=curPath.includes('/') ? curPath.substring(0,curPath.lastIndexOf('/')) : ''; + html+=`
+ + .. + +
`; + } + if (!entries.length) { body.innerHTML=html+'
Ordner ist leer
'; return; } + + entries.forEach(e=>{ + const icon=e.dir ? '📁' : fileIcon(e.name); + const size=e.size!=null ? fmtSize(e.size) : ''; + const newPath=(curPath?curPath+'/':'')+e.name; + const click=e.dir ? `onclick="expl.navigate('${newPath.replace(/'/g,"\\'")}') "` : ''; + html+=`
+ ${icon} + ${e.name} + ${size} + ${e.mtime||''} +
`; + }); + body.innerHTML=html; + } +}; + +function fileIcon(n) { + const e=(n.split('.').pop()||'').toLowerCase(); + if(['jpg','jpeg','png','gif','bmp','raw','cr2','nef','arw','heic','webp','dng'].includes(e)) return '🖼'; + if(['mp4','mov','avi','mkv','mts','m2ts','wmv','3gp'].includes(e)) return '🎬'; + if(['mp3','wav','flac','aac','m4a','ogg','wma'].includes(e)) return '🎵'; + if(['pdf','doc','docx','txt','xls','xlsx','ppt','pptx'].includes(e)) return '📄'; + if(['zip','rar','7z','tar','gz','bz2'].includes(e)) return '🗜'; + return '📄'; +} +function fmtSize(b) { + if(b==null) return ''; + if(b<1024) return b+' B'; + if(b<1048576) return (b/1024).toFixed(1)+' KB'; + if(b<1073741824) return (b/1048576).toFixed(1)+' MB'; + return (b/1073741824).toFixed(2)+' GB'; } // ── Poll ────────────────────────────────────────────────────────────────── async function poll() { try { - const {copy:c, wifi:w} = await api('/status'); + const {copy:c,wifi:w}=await api('/status'); + const dot=$('wifi-dot'),mTxt=$('wifi-mode-txt'),ip=$('wifi-ip'); + if(w.mode==='client'){dot.className='wifi-dot green';mTxt.innerHTML='🔌 '+(w.ssid||'Verbunden');ip.textContent=w.ip||'';} + else if(w.mode==='ap'){dot.className='wifi-dot blue';mTxt.innerHTML='📶 Hotspot: '+(w.ssid||'PiCopy');ip.textContent='10.42.0.1 · Port 8080';} + else{dot.className='wifi-dot grey';mTxt.textContent='Kein WLAN';ip.textContent='';} - // WiFi bar - const dot=$('wifi-dot'), mTxt=$('wifi-mode-txt'), ip=$('wifi-ip'); - if (w.mode==='client'){ - dot.className='wifi-dot green'; - mTxt.innerHTML='🔌 '+(w.ssid||'Verbunden'); - ip.textContent=w.ip||''; - } else if (w.mode==='ap'){ - dot.className='wifi-dot blue'; - mTxt.innerHTML='📶 Hotspot: '+(w.ssid||'PiCopy'); - ip.textContent='10.42.0.1 · Port 8080'; - } else { - dot.className='wifi-dot grey'; - mTxt.textContent='Kein WLAN'; ip.textContent=''; - } - - // Copy status - const txt=$('st-text'), bar=$('prog-bar'), wrap=$('prog-wrap'); - const info=$('prog-info'), sum=$('st-summary'); - const bS=$('btn-start'), bC=$('btn-cancel'); - if (c.running){ - txt.className='st-run'; txt.textContent='Kopiert… '+c.progress+'%'; - wrap.style.display='block'; bar.style.width=c.progress+'%'; + const txt=$('st-text'),bar=$('prog-bar'),wrap=$('prog-wrap'),info=$('prog-info'),sum=$('st-summary'); + const bS=$('btn-start'),bC=$('btn-cancel'); + if(c.running){ + txt.className='st-run';txt.textContent='Kopiert… '+c.progress+'%'; + wrap.style.display='block';bar.style.width=c.progress+'%'; info.textContent=c.current?c.done+' / '+c.total+' — '+c.current:''; - sum.textContent=''; bS.style.display='none'; bC.style.display=''; + sum.textContent='';bS.style.display='none';bC.style.display=''; } else { - bS.style.display=''; bC.style.display='none'; info.textContent=''; - if (c.error){ - txt.className='st-err'; txt.textContent='Fehler: '+c.error; - wrap.style.display='none'; sum.textContent=''; - } else if (c.last_copy){ - txt.className='st-ok'; txt.textContent='✓ Abgeschlossen'; - wrap.style.display='block'; bar.style.width='100%'; - sum.textContent=c.total+' Dateien · '+new Date(c.last_copy).toLocaleString('de-DE'); - } else { - txt.className='st-idle'; txt.textContent='Bereit'; - wrap.style.display='none'; sum.textContent=''; - } + bS.style.display='';bC.style.display='none';info.textContent=''; + if(c.error){txt.className='st-err';txt.textContent='Fehler: '+c.error;wrap.style.display='none';sum.textContent='';} + else if(c.last_copy){txt.className='st-ok';txt.textContent='✓ Abgeschlossen';wrap.style.display='block';bar.style.width='100%';sum.textContent=c.total+' Dateien · '+new Date(c.last_copy).toLocaleString('de-DE');} + else{txt.className='st-idle';txt.textContent='Bereit';wrap.style.display='none';sum.textContent='';} } - - // Log - if (c.logs&&c.logs.length) - $('log-box').innerHTML=c.logs.slice().reverse().map(l=> - `
${l.t}${l.m}
`).join(''); + if(c.logs&&c.logs.length) + $('log-box').innerHTML=c.logs.slice().reverse().map(l=>`
${l.t}${l.m}
`).join(''); } catch(e){} } -function flash(id, cls, msg) { - const el=$(id); el.className='flash '+cls; el.textContent=msg; el.style.display='block'; - if (cls==='ok') setTimeout(()=>el.style.display='none',3500); -} +function flash(id,cls,msg){const el=$(id);el.className='flash '+cls;el.textContent=msg;el.style.display='block';if(cls==='ok')setTimeout(()=>el.style.display='none',3500);} (async()=>{ await loadCfg(); await refreshDevices(); - setInterval(poll, 1500); - setInterval(refreshDevices, 8000); + expl.load(''); + setInterval(poll,1500); + setInterval(refreshDevices,8000); poll(); })();