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:
81
index.html
81
index.html
@@ -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('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>
|
||||
|
||||
Reference in New Issue
Block a user