dudi/docs/superpowers/plans/2026-04-17-refactor-collapsible.md

563 lines
29 KiB
Markdown
Raw Normal View History

# Zieltracker Refactor & Collapsible Cards Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Split index.html into three files, add collapsible cards with compact meta-line, sort by daily completion status, replace 4-block stats with a "Heute" group, and constrain desktop width to 480px.
**Architecture:** Pure HTML/CSS/JS — no build step. Logic stays in `app.js`, styles in `style.css`, HTML skeleton in `index.html`. All changes are in-place; localStorage data format is unchanged.
**Tech Stack:** Vanilla JS (ES5), CSS custom properties, localStorage
---
## File Map
| File | Action | Responsibility |
|------|--------|---------------|
| `index.html` | Modify | HTML skeleton only — links to style.css + app.js |
| `style.css` | Create | All styles including new classes |
| `app.js` | Create | All logic including new collapse/sort/render |
---
### Task 1: Create style.css
**Files:**
- Create: `style.css`
- [ ] **Step 1: Create style.css** — extract the full `<style>` block from index.html (lines 12103) and add new classes at the bottom:
```css
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600&family=DM+Mono:wght@400;500&display=swap');
:root{--bg:#f5f4f0;--bg2:#fff;--bg3:#f0eeea;--border:rgba(0,0,0,.07);--border2:rgba(0,0,0,.12);--text:#1a1a1a;--text2:#666;--text3:#aaa;--green:#16a34a;--green-bg:rgba(22,163,74,.08);--blue:#2563eb;--blue-bg:rgba(37,99,235,.08);--amber:#d97706;--amber-bg:rgba(217,119,6,.08);--red:#dc2626;--red-bg:rgba(220,38,38,.08);--r:14px;--rs:8px}
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--text);min-height:100dvh;padding-bottom:80px}
.main-wrap{max-width:480px;margin:0 auto}
.hdr{position:sticky;top:0;z-index:100;background:rgba(245,244,240,.92);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid var(--border);padding:16px 20px 14px;display:flex;align-items:center;justify-content:space-between}
.hdr-title{font-size:18px;font-weight:600;letter-spacing:-.3px}
.hdr-sub{font-size:12px;color:var(--text3);margin-top:1px;font-family:'DM Mono',monospace}
.hdr-btns{display:flex;gap:8px;align-items:center}
.btn-add{width:38px;height:38px;border-radius:50%;background:var(--text);color:var(--bg);border:none;cursor:pointer;font-size:22px;display:flex;align-items:center;justify-content:center;font-weight:300;transition:transform .15s}
.btn-add:active{transform:scale(.92)}
.btn-menu{width:38px;height:38px;border-radius:50%;background:var(--bg3);color:var(--text2);border:1px solid var(--border);cursor:pointer;font-size:20px;display:flex;align-items:center;justify-content:center;transition:transform .15s}
.btn-menu:active{transform:scale(.92)}
.main{padding:16px 16px 0}
.sec-lbl{font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:.6px;font-family:'DM Mono',monospace;margin-bottom:6px;margin-top:4px;padding:0 2px}
.empty{text-align:center;padding:60px 20px;color:var(--text3);font-size:14px;line-height:1.8}
.card{background:var(--bg2);border:1px solid var(--border);border-radius:var(--r);margin-bottom:8px;overflow:hidden}
.card.done{opacity:.7}
.card-hdr{padding:14px 16px 12px;display:flex;align-items:center;justify-content:space-between;gap:10px;cursor:pointer;user-select:none}
.name-wrap{display:flex;align-items:center;gap:6px}
.goal-name{font-size:16px;font-weight:600;letter-spacing:-.2px}
.btn-ren{background:none;border:none;color:var(--text3);cursor:pointer;font-size:14px;padding:2px 4px;line-height:1;flex-shrink:0}
.btn-ren:active{color:var(--blue)}
.ren-input{font-family:'DM Sans',sans-serif;font-size:16px;font-weight:600;background:var(--bg3);border:1px solid var(--blue);border-radius:6px;color:var(--text);padding:2px 8px;width:100%;letter-spacing:-.2px}
.ren-input:focus{outline:none}
.goal-meta{font-size:11px;color:var(--text3);margin-top:3px;font-family:'DM Mono',monospace;line-height:1.5}
.chevron{font-size:13px;color:var(--text3);margin-left:4px;flex-shrink:0}
.badge{font-size:10px;font-weight:600;padding:3px 8px;border-radius:99px;white-space:nowrap;flex-shrink:0;letter-spacing:.3px;text-transform:uppercase}
.b-ok{background:var(--green-bg);color:var(--green)}.b-done{background:var(--green-bg);color:var(--green)}.b-warn{background:var(--amber-bg);color:var(--amber)}.b-danger{background:var(--red-bg);color:var(--red)}.b-buf{background:var(--blue-bg);color:var(--blue)}
.prog-wrap{padding:0 16px 12px}
.prog-track{height:4px;background:var(--bg3);border-radius:99px;overflow:hidden}
.prog-fill{height:100%;border-radius:99px;transition:width .4s ease}
.prog-row{display:flex;justify-content:space-between;font-size:11px;color:var(--text3);margin-top:5px;font-family:'DM Mono',monospace}
.heute-stats{padding:0 16px 14px}
.heute-group{background:var(--bg3);border-radius:var(--rs);padding:8px 8px 6px}
.heute-lbl{font-size:9px;color:var(--text3);text-transform:uppercase;letter-spacing:.5px;text-align:center;margin-bottom:6px;font-family:'DM Mono',monospace}
.heute-inner{display:grid;grid-template-columns:1fr 1fr 1fr;gap:5px}
.stat{background:var(--bg2);border-radius:6px;padding:7px 6px 5px;text-align:center}
.slbl{font-size:9px;color:var(--text3);margin-bottom:2px}
.sval{font-size:14px;font-weight:600;font-family:'DM Mono',monospace}
.sunit{font-size:8px;color:var(--text3)}
.dots-sec{padding:0 16px 12px}
.dots-lbl{font-size:11px;color:var(--text3);margin-bottom:6px}
.dots-wrap{display:flex;gap:3px;flex-wrap:wrap}
.dot{width:26px;height:26px;border-radius:5px;display:flex;align-items:center;justify-content:center;font-size:8px;font-weight:600;font-family:'DM Mono',monospace;transition:opacity .15s}
.df{background:var(--bg3);color:var(--text3)}
.dm{background:rgba(220,38,38,.12);color:var(--red)}
.dp{background:rgba(217,119,6,.12);color:var(--amber)}
.dd{background:rgba(22,163,74,.12);color:var(--green)}
.db{background:rgba(37,99,235,.12);color:var(--blue)}
.de{cursor:pointer}.de:active{opacity:.6;transform:scale(.9)}.dl{opacity:.6;cursor:default}
.rt{box-shadow:0 0 0 2px var(--text2)}.ry{box-shadow:0 0 0 2px var(--text3)}.rs{box-shadow:0 0 0 2px var(--blue) !important}
.legend{display:flex;gap:10px;flex-wrap:wrap;margin-top:8px}
.leg{display:flex;align-items:center;gap:4px;font-size:10px;color:var(--text3)}
.ldot{width:8px;height:8px;border-radius:2px}
.dpanel{margin:0 16px 14px;background:var(--bg3);border-radius:var(--rs);overflow:hidden;border:1px solid var(--border2)}
.dpanel-hdr{padding:10px 12px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--border)}
.dpanel-title{font-size:13px;font-weight:600}
.dpanel-sub{font-size:11px;color:var(--text2);font-family:'DM Mono',monospace}
.dpanel-body{padding:10px 12px}
.set-row{display:flex;align-items:center;justify-content:space-between;padding:5px 0;border-bottom:1px solid var(--border);font-size:13px}
.set-row:last-of-type{border-bottom:none}
.stime{font-size:10px;color:var(--text3);font-family:'DM Mono',monospace;margin-right:4px}
.sdel{background:none;border:none;color:var(--text3);cursor:pointer;font-size:16px;padding:2px 6px;border-radius:4px}
.sdel:active{color:var(--red)}
.add-row{display:flex;gap:8px;align-items:center;margin-top:10px}
.num-in{font-family:'DM Mono',monospace;font-size:15px;padding:8px 10px;border-radius:var(--rs);border:1px solid var(--border2);background:var(--bg2);color:var(--text);width:90px;text-align:center}
.num-in:focus{outline:none;border-color:var(--blue)}
.ulbl{font-size:12px;color:var(--text3)}
.btn-as{flex:1;padding:8px;border-radius:var(--rs);background:var(--blue-bg);color:var(--blue);border:1px solid rgba(37,99,235,.2);cursor:pointer;font-family:'DM Sans',sans-serif;font-size:13px;font-weight:600}
.btn-as:active{opacity:.7}
.nosets{font-size:12px;color:var(--text3);padding:4px 0 8px}
.card-foot{padding:8px 16px 12px;display:flex;justify-content:flex-end}
.btn-del{background:none;border:none;color:var(--text3);font-size:12px;cursor:pointer;padding:4px 0;font-family:'DM Sans',sans-serif}
.btn-del:active{color:var(--red)}
.ov{position:fixed;inset:0;z-index:200;background:rgba(0,0,0,.5);display:flex;align-items:flex-end;animation:fi .2s ease}
@keyframes fi{from{opacity:0}to{opacity:1}}
.sheet{width:100%;background:var(--bg2);border-radius:20px 20px 0 0;border:1px solid var(--border2);padding:20px 20px 40px;animation:su .25s ease;max-height:90dvh;overflow-y:auto}
@keyframes su{from{transform:translateY(100%)}to{transform:translateY(0)}}
.shandle{width:36px;height:4px;border-radius:2px;background:var(--border2);margin:0 auto 20px}
.stitle{font-size:17px;font-weight:600;margin-bottom:4px}
.ssub{font-size:12px;color:var(--text3);margin-bottom:16px}
.fgrid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px}
.ff{margin-bottom:10px}
.ff label{font-size:11px;color:var(--text3);display:block;margin-bottom:4px;text-transform:uppercase;letter-spacing:.5px}
.fi{width:100%;font-family:'DM Sans',sans-serif;font-size:15px;padding:10px 12px;border-radius:var(--rs);border:1px solid var(--border2);background:var(--bg3);color:var(--text)}
.fi:focus{outline:none;border-color:var(--blue)}
.factions{display:flex;gap:8px;margin-top:16px}
.btn-p{flex:1;padding:13px;border-radius:var(--rs);background:var(--text);color:var(--bg);border:none;cursor:pointer;font-family:'DM Sans',sans-serif;font-size:15px;font-weight:600}
.btn-p:active{opacity:.8}
.btn-c{padding:13px 20px;border-radius:var(--rs);background:var(--bg3);color:var(--text2);border:1px solid var(--border);cursor:pointer;font-family:'DM Sans',sans-serif;font-size:15px}
.dbtn{width:100%;padding:13px 16px;border-radius:var(--rs);background:var(--bg3);border:1px solid var(--border);color:var(--text);font-family:'DM Sans',sans-serif;font-size:15px;text-align:left;cursor:pointer;margin-bottom:8px;display:flex;align-items:center;gap:12px}
.dbtn:active{background:var(--border2)}
.dico{font-size:18px;width:24px;text-align:center;flex-shrink:0}
.dlbl{flex:1}.dsub{font-size:11px;color:var(--text3);display:block;margin-top:1px}
.ddanger{color:var(--red)}
.ddiv{height:1px;background:var(--border);margin:12px 0}
.hint{margin:0 16px 12px;background:var(--blue-bg);border:1px solid rgba(37,99,235,.15);border-radius:var(--rs);padding:10px 12px;font-size:12px;color:var(--blue);line-height:1.5;display:flex;justify-content:space-between;align-items:center;gap:8px}
.hclose{background:none;border:none;color:var(--blue);cursor:pointer;font-size:16px;padding:0 2px}
```
- [ ] **Step 2: Verify file exists**
```bash
ls -la /srv/http/zieltracker/style.css
```
Expected: file exists, ~4KB
---
### Task 2: Create app.js
**Files:**
- Create: `app.js`
- [ ] **Step 1: Create app.js** with the full logic — extracted from index.html plus all new functionality:
```js
var TODAY = new Date(); TODAY.setHours(0,0,0,0);
var goals, prefs, selDay={}, addAmt={}, renamingId=null, renameVal='', collapsed={};
function load(k,def){ try{ return JSON.parse(localStorage.getItem(k)||def); }catch(e){ return JSON.parse(def); } }
function save(){ localStorage.setItem('zt_g',JSON.stringify(goals)); }
function saveP(){ localStorage.setItem('zt_p',JSON.stringify(prefs)); }
goals = load('zt_g','[]');
prefs = load('zt_p','{}');
function tOff(g){ return Math.round((TODAY - new Date(g.start))/86400000); }
function o2d(g,i){ var d=new Date(new Date(g.start).getTime()+i*86400000); d.setHours(0,0,0,0); return d; }
function dTot(g,o){ return (g.sets[String(o)]||[]).reduce(function(a,b){return a+b.amount;},0); }
function fd(d){ return d.toLocaleDateString('de-DE',{weekday:'short',day:'numeric',month:'short'}); }
function fs(d){ return d.toLocaleDateString('de-DE',{day:'numeric',month:'short'}); }
function editable(g,o){ var t=tOff(g); return o===t||o===t-1; }
function now(){ var n=new Date(); return String(n.getHours()).padStart(2,'0')+':'+String(n.getMinutes()).padStart(2,'0'); }
// Returns CSS color for heute: X/Y display
function heuteColor(tdone, daily){
if(tdone===0) return 'var(--red)';
if(tdone>=daily*1.1) return 'var(--blue)';
if(tdone>=daily) return 'var(--green)';
return 'var(--amber)';
}
// collapsed defaults to true — only false when explicitly opened
function isCollapsed(id){ return collapsed[id]!==false; }
function toggleCollapse(id){ collapsed[id]=!isCollapsed(id); render(); }
function calc(g){
var t=tOff(g), tot=g.daily*g.days;
var dr=Math.max(0,g.days-t-1);
var sd=new Date(g.start); sd.setHours(0,0,0,0);
var end=new Date(sd.getTime()+g.days*86400000);
var past=0;
for(var i=0;i<Math.min(t,g.days);i++) past+=dTot(g,i);
var tdone=dTot(g,t), tot2=past+tdone;
var dl=dr+1;
var remaining=Math.max(0,tot-past);
var pd=Math.ceil(remaining/Math.max(1,dl));
var st=Math.max(0,pd-tdone);
var expectedPast=Math.min(t,g.days)*g.daily;
var buf=(past-expectedPast)+Math.max(0,tdone-g.daily);
var deficit=Math.min(0,buf);
var surplus=Math.max(0,buf);
var dailyDelta=pd-g.daily;
var pct=Math.min(100,Math.round((tot2/tot)*100));
return{tot:tot,tOff:t,end:end,dr:dr,done:tot2,tdone:tdone,pd:pd,st:st,buf:buf,deficit:deficit,surplus:surplus,dailyDelta:dailyDelta,net:tdone-pd,pct:pct,ok:tdone>=pd};
}
function dcls(g,i){
var t=tOff(g); if(i>t) return 'dot df';
var v=dTot(g,i);
var c=v===0?'dot dm':v>=g.daily*1.1?'dot db':v>=g.daily?'dot dd':'dot dp';
return c+(editable(g,i)?' de':' dl');
}
function dlbl(g,i){
var t=tOff(g); if(i>t) return String(i+1);
var v=dTot(g,i);
if(v===0) return '✕'; if(v>=g.daily*1.1) return '+'; if(v>=g.daily) return '✓';
return Math.round(v/g.daily*100)+'%';
}
function addSet(gid,off){
var g=goals.filter(function(x){return x.id===gid;})[0];
if(!g||!editable(g,off)) return;
var k=gid+'_'+off, amt=parseInt(addAmt[k]||'0',10);
if(amt<=0) return;
if(!g.sets[String(off)]) g.sets[String(off)]=[];
g.sets[String(off)].push({amount:amt,time:off===tOff(g)?now():'—'});
addAmt[k]=''; save(); render();
}
function remSet(gid,off,idx){
var g=goals.filter(function(x){return x.id===gid;})[0];
if(!g||!editable(g,off)) return;
g.sets[String(off)].splice(idx,1); save(); render();
}
function delGoal(id){
if(!confirm('Ziel wirklich löschen?')) return;
goals=goals.filter(function(g){return g.id!==id;}); save(); render();
}
function selD(gid,off){
var g=goals.filter(function(x){return x.id===gid;})[0];
if(!g||!editable(g,off)) return;
selDay[gid]=selDay[gid]===off?null:off; render();
}
function startRen(id){
var g=goals.filter(function(x){return x.id===id;})[0]; if(!g) return;
renamingId=id; renameVal=g.name; render();
setTimeout(function(){ var el=document.getElementById('ri'+id); if(el){el.focus();el.select();} },50);
}
function commitRen(id){
var g=goals.filter(function(x){return x.id===id;})[0];
if(g&&renameVal.trim()) g.name=renameVal.trim();
renamingId=null; save(); render();
}
function cancelRen(){ renamingId=null; render(); }
function closeOv(){ var o=document.getElementById('ov'); o.style.display='none'; o.innerHTML=''; }
function showSheet(html){
var o=document.getElementById('ov');
o.style.cssText='display:flex;position:fixed;inset:0;z-index:200;background:rgba(0,0,0,.5);align-items:flex-end;animation:fi .2s ease';
o.innerHTML='<div class="sheet">'+html+'</div>';
o.onclick=function(e){if(e.target===o)closeOv();};
}
function openNew(){
showSheet(
'<div class="shandle"></div>'+
'<div class="stitle">Neues Ziel</div>'+
'<div class="ff"><label>Übung / Gewohnheit</label><input class="fi" id="fn" type="text" placeholder="Liegestütz, Plank …"/></div>'+
'<div class="fgrid">'+
'<div class="ff"><label>Einheit</label><input class="fi" id="fu" type="text" value="Stück"/></div>'+
'<div class="ff"><label>Tagesziel</label><input class="fi" id="fd" type="number" min="1" value="50"/></div>'+
'</div>'+
'<div class="ff"><label>Dauer in Tagen</label><input class="fi" id="fdy" type="number" min="7" max="365" value="30"/></div>'+
'<div class="factions"><button class="btn-p" id="fsub">Ziel starten</button><button class="btn-c" id="fcan">Abbrechen</button></div>'
);
document.getElementById('fn').focus();
document.getElementById('fsub').onclick=function(){
var name=(document.getElementById('fn').value||'').trim();
var unit=(document.getElementById('fu').value||'').trim()||'Stück';
var daily=parseInt(document.getElementById('fd').value,10)||1;
var days=parseInt(document.getElementById('fdy').value,10)||30;
if(!name){document.getElementById('fn').focus();return;}
goals.push({id:Date.now(),name:name,unit:unit,daily:daily,days:days,start:TODAY.toISOString(),sets:{}});
save(); closeOv(); render();
};
document.getElementById('fcan').onclick=closeOv;
}
function openData(){
showSheet(
'<div class="shandle"></div>'+
'<div class="stitle">Daten verwalten</div>'+
'<div class="ssub">Export, Import und Backup</div>'+
'<button class="dbtn" id="dexp"><span class="dico"></span><span class="dlbl">Exportieren<span class="dsub">Alle Ziele als JSON-Datei speichern</span></span></button>'+
'<button class="dbtn" id="dimp"><span class="dico"></span><span class="dlbl">Importieren<span class="dsub">Backup laden oder zusammenführen</span></span></button>'+
'<div class="ddiv"></div>'+
'<button class="dbtn ddanger" id="dclr"><span class="dico"></span><span class="dlbl">Alle Daten löschen<span class="dsub">Kann nicht rückgängig gemacht werden</span></span></button>'+
'<button class="btn-c" id="dcls" style="width:100%;margin-top:4px;text-align:center">Schließen</button>'
);
document.getElementById('dcls').onclick=closeOv;
document.getElementById('dexp').onclick=function(){
var blob=new Blob([JSON.stringify({goals:goals,at:new Date().toISOString()},null,2)],{type:'application/json'});
var url=URL.createObjectURL(blob), a=document.createElement('a');
a.href=url; a.download='zieltracker-backup.json'; a.click(); URL.revokeObjectURL(url); closeOv();
};
document.getElementById('dimp').onclick=function(){
var inp=document.createElement('input'); inp.type='file'; inp.accept='.json';
inp.onchange=function(e){
var f=e.target.files[0]; if(!f) return;
var r=new FileReader(); r.onload=function(ev){
try{
var p=JSON.parse(ev.target.result);
if(!p.goals||!Array.isArray(p.goals)) throw new Error('Ungültiges Format');
if(!confirm(p.goals.length+' Ziel(e) importieren? Bestehende Daten bleiben erhalten.')) return;
var ex={}; goals.forEach(function(g){ex[g.id]=1;});
var add=0; p.goals.forEach(function(g){if(!ex[g.id]){goals.push(g);add++;}});
save(); closeOv(); render(); alert(add+' Ziel(e) importiert.');
}catch(err){alert('Fehler: '+err.message);}
}; r.readAsText(f);
}; inp.click();
};
document.getElementById('dclr').onclick=function(){
if(!confirm('Alle Daten löschen?')) return;
goals=[]; save(); closeOv(); render();
};
}
function panel(g,off){
var t=tOff(g), sets=g.sets[String(off)]||[], tot=dTot(g,off);
var lbl=off===t?'Heute':'Gestern', k=g.id+'_'+off;
var rows='';
for(var i=0;i<sets.length;i++){
var s=sets[i];
rows+='<div class="set-row"><span>'+(s.time!=='—'?'<span class="stime">'+s.time+' ·</span>':'')+' <strong>'+s.amount+'</strong> '+g.unit+'</span>'+
'<button class="sdel" data-g="'+g.id+'" data-o="'+off+'" data-i="'+i+'">×</button></div>';
}
return '<div class="dpanel">'+
'<div class="dpanel-hdr"><span class="dpanel-title">'+lbl+' — '+fd(o2d(g,off))+'</span><span class="dpanel-sub">'+tot+' / '+g.daily+' '+g.unit+'</span></div>'+
'<div class="dpanel-body">'+
(sets.length?rows:'<div class="nosets">Noch kein Eintrag</div>')+
'<div class="add-row">'+
'<input class="num-in" type="number" min="1" placeholder="'+g.daily+'" value="'+(addAmt[k]||'')+'" data-k="'+k+'" data-g="'+g.id+'" data-o="'+off+'"/>'+
'<span class="ulbl">'+g.unit+'</span>'+
'<button class="btn-as" data-g="'+g.id+'" data-o="'+off+'">+ Satz</button>'+
'</div>'+
'</div></div>';
}
function cardHtml(g){
var c=calc(g), t=c.tOff;
var fc=c.surplus>0?'var(--blue)':c.st===0?'var(--green)':c.dailyDelta<=0?'var(--green)':c.dailyDelta<=g.daily*.2?'var(--amber)':'var(--red)';
var bc,bt;
if(c.ok&&c.surplus>0){bc='b-buf';bt='+'+c.surplus+' Puffer';}
else if(c.ok){bc='b-done';bt='Heute ✓';}
else if(c.dailyDelta<=0){bc='b-ok';bt='Im Plan';}
else if(c.dailyDelta<=g.daily*.2){bc='b-warn';bt='Leicht mehr';}
else{bc='b-danger';bt='-'+Math.abs(c.deficit)+' Rückstand';}
var nh=renamingId===g.id
?'<div class="name-wrap"><input class="ren-input" id="ri'+g.id+'" value="'+g.name.replace(/"/g,'&quot;')+'" data-g="'+g.id+'"/></div>'
:'<div class="name-wrap"><div class="goal-name">'+g.name+'</div><button class="btn-ren" data-g="'+g.id+'"></button></div>';
var doneCls=c.ok?' done':'';
if(isCollapsed(g.id)){
var hc=heuteColor(c.tdone,g.daily);
var metaC='Noch '+c.dr+'T · endet '+fs(c.end)+' · heute: <span style="color:'+hc+'">'+c.tdone+'/'+g.daily+'</span> · total: '+c.done+'/'+c.tot;
return '<div class="card'+doneCls+'">'+
'<div class="card-hdr" data-g="'+g.id+'">'+
'<div style="flex:1;min-width:0">'+nh+'<div class="goal-meta">'+metaC+'</div></div>'+
'<span class="badge '+bc+'">'+bt+'</span>'+
'<span class="chevron"></span>'+
'</div>'+
'<div style="padding:0 16px 12px"><div class="prog-track"><div class="prog-fill" style="width:'+c.pct+'%;background:'+fc+'"></div></div></div>'+
'</div>';
}
// Expanded
var sel=selDay[g.id];
var dots='';
for(var i=0;i<g.days;i++){
var it=i===t, iy=i===t-1, is=sel===i, ed=editable(g,i);
var cls=dcls(g,i)+(is?' rs':it?' rt':iy&&t>0?' ry':'');
dots+='<div class="'+cls+'"'+(ed?' data-g="'+g.id+'" data-d="'+i+'"':'')+'>'+dlbl(g,i)+'</div>';
}
var nc=heuteColor(c.tdone,g.daily);
return '<div class="card'+doneCls+'">'+
'<div class="card-hdr" data-g="'+g.id+'">'+
'<div style="flex:1;min-width:0">'+nh+'<div class="goal-meta">Noch '+c.dr+'T · endet '+fs(c.end)+'</div></div>'+
'<span class="badge '+bc+'">'+bt+'</span>'+
'<span class="chevron"></span>'+
'</div>'+
'<div class="prog-wrap"><div class="prog-track"><div class="prog-fill" style="width:'+c.pct+'%;background:'+fc+'"></div></div>'+
'<div class="prog-row"><span>'+c.done+' '+g.unit+' gemacht</span><span>'+c.pct+'% von '+c.tot+'</span></div></div>'+
'<div class="heute-stats"><div class="heute-group">'+
'<div class="heute-lbl">Heute</div>'+
'<div class="heute-inner">'+
'<div class="stat"><div class="slbl">Gemacht</div><div class="sval">'+c.tdone+'<div class="sunit">'+g.unit+'</div></div></div>'+
'<div class="stat"><div class="slbl">Tagesziel</div><div class="sval">'+g.daily+'<div class="sunit">'+g.unit+'</div></div></div>'+
'<div class="stat"><div class="slbl">Noch</div><div class="sval" style="color:'+nc+'">'+c.st+'<div class="sunit">'+g.unit+'</div></div></div>'+
'</div>'+
'</div></div>'+
'<div class="dots-sec"><div class="dots-lbl">Verlauf — heute &amp; gestern bearbeitbar</div>'+
'<div class="dots-wrap">'+dots+'</div>'+
'<div class="legend">'+
'<span class="leg"><span class="ldot" style="background:rgba(37,99,235,.3)"></span>Puffer</span>'+
'<span class="leg"><span class="ldot" style="background:rgba(22,163,74,.3)"></span>Erreicht</span>'+
'<span class="leg"><span class="ldot" style="background:rgba(217,119,6,.3)"></span>Teilweise</span>'+
'<span class="leg"><span class="ldot" style="background:rgba(220,38,38,.3)"></span>Verpasst</span>'+
'</div></div>'+
(sel!=null?panel(g,sel):'')+
'<div class="card-foot"><button class="btn-del" data-g="'+g.id+'">Ziel löschen</button></div>'+
'</div>';
}
function render(){
var m=document.getElementById('main'), h='';
if(!prefs.hd) h+='<div class="hint">Menü → "Zum Startbildschirm" für App-Icon<button class="hclose" id="hc">×</button></div>';
if(!goals.length){
h+='<div class="empty"><div style="font-size:40px;opacity:.4;margin-bottom:12px">🎯</div>Noch keine Ziele.<br>Tippe auf <strong>+</strong> um zu starten.</div>';
m.innerHTML=h; wire(); return;
}
// Sort: open first, done today at bottom
var open=[], done=[];
for(var gi=0;gi<goals.length;gi++){
var g=goals[gi], c=calc(g);
if(c.ok) done.push(g); else open.push(g);
}
if(open.length){
h+='<div class="sec-lbl">Offen</div>';
for(var i=0;i<open.length;i++) h+=cardHtml(open[i]);
}
if(done.length){
h+='<div class="sec-lbl">Heute erledigt</div>';
for(var j=0;j<done.length;j++) h+=cardHtml(done[j]);
}
m.innerHTML=h; wire();
}
function wire(){
var hc=document.getElementById('hc');
if(hc) hc.onclick=function(){prefs.hd=1;saveP();this.closest('.hint').remove();};
document.querySelectorAll('.card-hdr[data-g]').forEach(function(el){
el.onclick=function(e){
if(e.target.classList.contains('btn-ren')||e.target.classList.contains('ren-input')) return;
toggleCollapse(parseInt(this.dataset.g,10));
};
});
document.querySelectorAll('.btn-ren').forEach(function(b){
b.onclick=function(e){e.stopPropagation();startRen(parseInt(this.dataset.g,10));};
});
document.querySelectorAll('.ren-input').forEach(function(inp){
var gid=parseInt(inp.dataset.g,10);
inp.oninput=function(){renameVal=this.value;};
inp.onkeydown=function(e){if(e.key==='Enter')commitRen(gid);if(e.key==='Escape')cancelRen();};
inp.onblur=function(){commitRen(gid);};
});
document.querySelectorAll('.de').forEach(function(d){
d.onclick=function(e){e.stopPropagation();selD(parseInt(this.dataset.g,10),parseInt(this.dataset.d,10));};
});
document.querySelectorAll('.btn-as').forEach(function(b){
b.onclick=function(){addSet(parseInt(this.dataset.g,10),parseInt(this.dataset.o,10));};
});
document.querySelectorAll('.num-in').forEach(function(inp){
var k=inp.dataset.k, g=parseInt(inp.dataset.g,10), o=parseInt(inp.dataset.o,10);
inp.oninput=function(){addAmt[k]=this.value;};
inp.onkeydown=function(e){if(e.key==='Enter')addSet(g,o);};
});
document.querySelectorAll('.sdel').forEach(function(b){
b.onclick=function(){remSet(parseInt(this.dataset.g,10),parseInt(this.dataset.o,10),parseInt(this.dataset.i,10));};
});
document.querySelectorAll('.btn-del').forEach(function(b){
b.onclick=function(){delGoal(parseInt(this.dataset.g,10));};
});
}
document.getElementById('btnNew').onclick=openNew;
document.getElementById('btnData').onclick=openData;
document.getElementById('tlbl').textContent=TODAY.toLocaleDateString('de-DE',{weekday:'long',day:'numeric',month:'long'});
render();
function scheduleMidnight(){
var n=new Date();
var ms=new Date(n.getFullYear(),n.getMonth(),n.getDate()+1,0,0,5).getTime()-n.getTime();
setTimeout(function(){
TODAY=new Date();TODAY.setHours(0,0,0,0);selDay={};collapsed={};
document.getElementById('tlbl').textContent=TODAY.toLocaleDateString('de-DE',{weekday:'long',day:'numeric',month:'long'});
render();scheduleMidnight();
},ms);
}
scheduleMidnight();
document.addEventListener('visibilitychange',function(){
if(document.visibilityState==='visible'){
var n=new Date();n.setHours(0,0,0,0);
if(n.getTime()!==TODAY.getTime()){TODAY=n;selDay={};collapsed={};render();scheduleMidnight();}
}
});
```
- [ ] **Step 2: Verify file exists**
```bash
ls -la /srv/http/zieltracker/app.js
```
Expected: file exists, ~8KB
---
### Task 3: Rewrite index.html as clean skeleton
**Files:**
- Modify: `index.html`
- [ ] **Step 1: Replace index.html** with a clean skeleton that references the new files:
```html
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
<meta name="theme-color" content="#f5f4f0"/>
<meta name="mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<title>Zieltracker</title>
<link rel="stylesheet" href="style.css"/>
</head>
<body>
<div class="main-wrap">
<div class="hdr">
<div>
<div class="hdr-title">Zieltracker</div>
<div class="hdr-sub" id="tlbl"></div>
</div>
<div class="hdr-btns">
<button class="btn-menu" id="btnData"></button>
<button class="btn-add" id="btnNew">+</button>
</div>
</div>
<div class="main" id="main"></div>
</div>
<div id="ov" style="display:none"></div>
<script src="app.js"></script>
</body>
</html>
```
- [ ] **Step 2: Open in browser and verify**
Open `http://localhost/zieltracker/` (or wherever it's served).
Check:
- Page loads without console errors
- Header and cards are centered on a wide screen (max ~480px content width)
- Existing goals show up as collapsed with meta-line `Noch XT · endet DD. Mon · heute: X/Y · total: X/Y`
- Clicking a card header expands it, showing Heute-Stats group + Dots + Entry panel
- Clicking again collapses it
- Goals with `ok=true` (today's quota met) appear under "Heute erledigt" with reduced opacity
- Rename pencil still works (click pencil → edit inline)
- Dot click still opens entry panel
- `+ Satz` still saves a set
- [ ] **Step 3: Commit**
```bash
cd /srv/http/zieltracker
git add index.html style.css app.js
git commit -m "refactor: split into files, collapsible cards, heute stats, desktop max-width"
```