feat: Musik-Gruppe + Export/Import + launchd auto-start

- Add Musik group (Tiefgang, MDIM, DROII, Soloprojekt) to DEFAULTS + BOARD_META
- Ensure Musik group survives localStorage reset via loadGroups() guard
- Add Export/Import buttons in sidebar (teal/amber) — full localStorage snapshot as JSON
- Add launchd plist (de.robinchoice.kanban-server) for auto-start on login at port 8765

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Robin Choice
2026-05-20 16:18:37 +02:00
parent 9e3ce14d81
commit 670f9c999e

View File

@@ -38,6 +38,10 @@ body { background:var(--bg); color:var(--text); font-family:-apple-system,BlinkM
.nav-badge.blk { background:var(--red-soft); color:var(--red); }
.s-reset { margin:6px 14px 0; padding:5px 10px; background:var(--surface3); border:1px solid var(--border); border-radius:5px; font-size:10px; color:var(--text-dim); cursor:pointer; text-align:center; }
.s-reset:hover { color:var(--red); border-color:var(--red); }
.s-io { display:flex; gap:6px; margin:6px 14px 0; }
.s-io-btn { flex:1; text-align:center; padding:5px 0; background:var(--surface3); border:1px solid var(--border); border-radius:5px; font-size:10px; color:var(--text-dim); cursor:pointer; }
.s-io-btn:hover { color:var(--teal); border-color:var(--teal); }
.s-io-btn.import:hover { color:var(--amber); border-color:var(--amber); }
.s-actions { display:flex; gap:6px; padding:10px 14px 0; }
.s-add-btn { flex:1; text-align:center; padding:5px 0; background:var(--surface2); border:1px solid var(--border2); border-radius:5px; font-size:10.5px; color:var(--text-dim); cursor:pointer; }
.s-add-btn:hover { color:var(--accent); border-color:var(--accent); }
@@ -393,6 +397,11 @@ body { background:var(--bg); color:var(--text); font-family:-apple-system,BlinkM
<div class="s-add-btn" onclick="showAddGroup()">+ Gruppe</div>
</div>
<div class="s-reset" onclick="resetCurrentBoard()">↺ Reset</div>
<div class="s-io">
<div class="s-io-btn" onclick="exportState()" title="Kompletten State als JSON exportieren">↓ Export</div>
<div class="s-io-btn import" onclick="document.getElementById('import-file').click()" title="State aus JSON-Datei laden">↑ Import</div>
</div>
<input type="file" id="import-file" accept=".json" style="display:none" onchange="importState(this)">
</nav>
<div class="main">
@@ -825,6 +834,42 @@ const DEFAULTS = {
{id:'done', label:'Done', tasks:[{t:'Schema-Diff: source_type, author, year, isbn',note:'Frontmatter-Schema + Template dokumentiert'}]},
]
},
tiefgang: {
name:'Tiefgang', goal:'',
wipLimit:3, throughput:2, sle:{days:14,p:85},
cols:[
{id:'ready', label:'Ready', tasks:[]},
{id:'wip', label:'In Progress', tasks:[]},
{id:'done', label:'Done', tasks:[]},
]
},
mdimmusic: {
name:'MDIM', goal:'mydrugismusic — Label & Artist-Kollektiv.',
wipLimit:3, throughput:2, sle:{days:14,p:85},
cols:[
{id:'ready', label:'Ready', tasks:[]},
{id:'wip', label:'In Progress', tasks:[]},
{id:'done', label:'Done', tasks:[]},
]
},
droii: {
name:'DROII', goal:'',
wipLimit:3, throughput:2, sle:{days:14,p:85},
cols:[
{id:'ready', label:'Ready', tasks:[]},
{id:'wip', label:'In Progress', tasks:[]},
{id:'done', label:'Done', tasks:[]},
]
},
soloprojekt: {
name:'Soloprojekt', goal:'',
wipLimit:3, throughput:2, sle:{days:14,p:85},
cols:[
{id:'ready', label:'Ready', tasks:[]},
{id:'wip', label:'In Progress', tasks:[]},
{id:'done', label:'Done', tasks:[]},
]
},
};
// ── BOARD META (group + color + ring for defaults) ────────────────────────
@@ -845,16 +890,21 @@ const BOARD_META = {
degoogle: {group:'Privat', color:'#60a5fa', ring:'1p'},
bibliothek:{group:'Privat', color:'#7c6af7', ring:'1p'},
aikb: {group:'Privat', color:'#4ade80', ring:'1w'},
tiefgang: {group:'Musik', color:'#2dd4bf', ring:'1p'},
mdimmusic: {group:'Musik', color:'#c084fc', ring:'1p'},
droii: {group:'Musik', color:'#f87171', ring:'1p'},
soloprojekt:{group:'Musik', color:'#fbbf24', ring:'1p'},
};
const RINGS = ['0','1p','1w','2','3','?'];
const RING_LABELS = {'0':'R0','1p':'R1p','1w':'R1w','2':'R2','3':'R3','?':'?'};
const PICK_COLORS = ['#f87171','#fb923c','#fbbf24','#4ade80','#2dd4bf','#60a5fa','#7c6af7','#c084fc'];
let GROUPS = ['Meta','Code','Beruflich','Web','Privat'];
let GROUPS = ['Meta','Code','Beruflich','Web','Privat','Musik'];
function loadGroups() {
try { const g = JSON.parse(localStorage.getItem('kanban_groups')); if (Array.isArray(g)) GROUPS = g; } catch {}
if (!GROUPS.includes('Meta')) { GROUPS.unshift('Meta'); saveGroups(); }
if (!GROUPS.includes('Musik')) { GROUPS.push('Musik'); saveGroups(); }
}
function saveGroups() { localStorage.setItem('kanban_groups', JSON.stringify(GROUPS)); }
@@ -2326,6 +2376,33 @@ loadIdeas();
renderIdeas();
show('doener');
setView('overview');
// ── EXPORT / IMPORT ──────────────────────────────────────────────────────────
function exportState() {
const keys = ['kanban_v2','kanban_groups','kanban_board_order','kanban_ideas','kanban_ideas_seeded'];
const snap = {};
keys.forEach(k => { const v = localStorage.getItem(k); if (v !== null) snap[k] = v; });
const blob = new Blob([JSON.stringify(snap, null, 2)], {type:'application/json'});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `kanban-backup-${new Date().toISOString().slice(0,10)}.json`;
a.click();
}
function importState(input) {
const file = input.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => {
try {
const snap = JSON.parse(e.target.result);
Object.entries(snap).forEach(([k, v]) => localStorage.setItem(k, v));
input.value = '';
location.reload();
} catch { alert('Ungültige Backup-Datei.'); }
};
reader.readAsText(file);
}
</script>
</body>
</html>