900|
   901|  /* ═══════════════════════════════════════
   902|     STORAGE & HELPERS
   903|     ═══════════════════════════════════════ */
   904|  const STORE      = 'zeitgeist-anki-v2';
   905|  const ABBR_STORE = 'vault-abbr';
   906|  const COLORS = ['#8B5CF6','#3B82F6','#10B981','#F59E0B','#EF4444','#EC4899','#14B8A6','#6366F1'];
   907|
   908|  /* ── ABBREVIATIONS (shared with Vault) ────────────── */
   909|  let _abbrs = [];
   910|  const abbrLoad = data => {
   911|    if (data && data[ABBR_STORE]) {
   912|      try { _abbrs = JSON.parse(data[ABBR_STORE]); } catch { _abbrs = []; }
   913|    } else {
   914|      try { _abbrs = JSON.parse(localStorage.getItem(ABBR_STORE) || '[]'); } catch { _abbrs = []; }
   915|    }
   916|  };
   917|  const applyAbbrs = s => {
   918|    _abbrs.forEach(a => {
   919|      if (!a.short || !a.long) return;
   920|      const esc_short = a.short.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
   921|      s = s.replace(new RegExp('\\b' + esc_short + '\\b', 'g'), a.long);
   922|    });
   923|    return s;
   924|  };
   925|
   926|  const load  = () => { try { return JSON.parse(localStorage.getItem(STORE)||'{"decks":{}}'); } catch { return {decks:{}}; } };
   927|  const save  = d => ZG.save(STORE, JSON.stringify(d));
   928|  const uid   = () => Date.now().toString(36) + Math.random().toString(36).slice(2,6);
   929|  const esc   = s => String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
   930|
   931|  /* ── RIS AUTO-LINK ────────────────────────────────── */
   932|  /* § / §§ — Paragraf-based laws */
   933|  const RIS_LAWS = {
   934|    'EStG':10004570,'KStG':10004569,'UStG':10004873,'BAO':10003940,
   935|    'ABGB':10001622,'GmbHG':10001720,'AktG':10002070,'UGB':10001702,'HGB':10001702,
   936|    'GewO':10007517,'ASVG':10008147,'FinStrG':10003898,'FLAG':10008220,
   937|    'GebG':10003999,'GrEStG':10004531,'KommStG':10004841,'ErbStG':10003850,
   938|    'StGB':10002296,'ZPO':10001699,'WTBG':20009983,'BiBuG':20008571,
   939|    'VerG':20001917,'IO':10001736,'KO':10001736,'AO':10001736,
   940|    'MRG':10002531,'WEG':20001921,'AVG':10005735,'VwGVG':20007887,
   941|    'DSG':10001597,'ECG':10007666,'TKG':20007904,'KSchG':10002462,
   942|    'ArbVG':10008374,'AngG':10008414,'AVRAG':10008775,'BMSVG':20003657,
   943|    'BörseG':20004329,'WAG':20006130,'BWG':20009885,'VersVG':10009994,
   944|    'KartG':20002697,'WettbG':20009623,'UrhG':10001848,'PatG':10002664,
   945|    'MSchG':10002526,'VStG':10005770,'JN':10001697,'PHG':10007990,
   946|    'EKHG':10009894,'IPRG':10002247,'BVergG':20007517,'StAG':10005400,
   947|    'WRG':10010290,'ForstG':10010056,'VwGG':10000990,
   948|  };
   949|  /* Art. — Artikel-based laws (B-VG, Verfassungsgesetze, etc.) */
   950|  const RIS_ART_LAWS = {
   951|    'B-VG':10000138,'StGG':10000006,'EMRK':10000308,
   952|    'VfGG':10000929,'VwGG':10000990,'MRK':10000308,
   953|    'MRKZP1':10001099,'MRKZP4':10001100,'MRKZP7':10001101,
   954|  };
   955|  const RIS_BASE = 'https://www.ris.bka.gv.at/NormDokument.wxe?Abfrage=Bundesnormen';
   956|
   957|  let _RIS_RE     = null;
   958|  let _RIS_ART_RE = null;
   959|
   960|  const _risDate = () => {
   961|    const d = new Date();
   962|    return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0');
   963|  };
   964|
   965|  const _buildRisRe = () => {
   966|    const alt = Object.keys(RIS_LAWS).map(k => k.replace(/[-]/g,'\\-')).join('|');
   967|    _RIS_RE = new RegExp(
   968|      '(§§\\s*\\d+[a-z]{0,2}(?:\\s*,\\s*\\d+[a-z]{0,2})*\\s+' +
   969|      '|§+\\s*\\d+[a-z]{0,2}\\s[^§\\n]*?)(' + alt + ')(?![A-Za-z\\u00C0-\\u024F0-9])', 'g'
   970|    );
   971|    const artAlt = Object.keys(RIS_ART_LAWS).map(k => k.replace(/[-]/g,'\\-')).join('|');
   972|    _RIS_ART_RE = new RegExp(
   973|      '(Art\\.?\\s*\\d+[a-z]{0,2}(?:\\s+Abs\\.?\\s*\\d+[a-z]{0,2})?)\\s+(' + artAlt + ')(?![A-Za-z\\u00C0-\\u024F0-9])', 'g'
   974|    );
   975|  };
   976|
   977|  const applyRisLinks = s => {
   978|    if (!_RIS_RE) _buildRisRe();
   979|    const ds = _risDate();
   980|    /* § / §§ links */
   981|    s = s.replace(_RIS_RE, (match, prefix, law) => {
   982|      const gn = RIS_LAWS[law]; if (!gn) return match;
   983|      if (prefix.charAt(1) === '§') {
   984|        const numsRaw = prefix.replace(/^§§\s*/,'').replace(/\s+$/,'');
   985|        return numsRaw.split(/\s*,\s*/).map(n => {
   986|          const m = n.trim().match(/^(\d+)([a-z]{1,2})?/);
   987|          const pn = m ? m[1] : '', ps = (m&&m[2]&&!/^ff?$/.test(m[2])) ? m[2] : '';
   988|          return `<a href="${RIS_BASE}&Gesetzesnummer=${gn}&FassungVom=${ds}&Artikel=&Paragraf=${pn+ps}&Anlage=&Uebergangsrecht=" target="_blank" rel="noopener" class="ris-link">§ ${n.trim()} ${law}</a>`;
   989|        }).join(', ');
   990|      } else {
   991|        const ms = prefix.match(/(\d+)([a-z]{1,2})?/);
   992|        const sn = ms ? ms[1] : '', ss = (ms&&ms[2]&&!/^ff?$/.test(ms[2])) ? ms[2] : '';
   993|        return `<a href="${RIS_BASE}&Gesetzesnummer=${gn}&FassungVom=${ds}&Artikel=&Paragraf=${sn+ss}&Anlage=&Uebergangsrecht=" target="_blank" rel="noopener" class="ris-link">${match}</a>`;
   994|      }
   995|    });
   996|    /* Art. links */
   997|    s = s.replace(_RIS_ART_RE, (match, artPart, law) => {
   998|      const gn = RIS_ART_LAWS[law]; if (!gn) return match;
   999|      const nm = artPart.match(/(\d+)([a-z]{0,2})?/);
  1000|      const an = nm ? nm[1] + (nm[2]&&!/^ff?$/.test(nm[2])?nm[2]:'') : '';
  1001|      return `<a href="${RIS_BASE}&Gesetzesnummer=${gn}&FassungVom=${ds}&Artikel=${an}&Paragraf=&Anlage=&Uebergangsrecht=" target="_blank" rel="noopener" class="ris-link">${match}</a>`;
  1002|    });
  1003|    return s;
  1004|  };
  1005|
  1006|  /* ── INLINE FORMATTING ─────────────────────────────── */
  1007|  const inlineFmt = raw => {
  1008|    const codes = [];
  1009|    /* protect inline code first */
  1010|    let s = raw.replace(/`([^`\n]+)`/g, (_, c) => { codes.push(''+esc(c)+''); return '\x00c'+(codes.length-1)+'\x00'; });
  1011|    /* escape html outside code spans */
  1012|    const parts = s.split(/(\x00c\d+\x00)/);
  1013|    s = parts.map(p => /^\x00c\d+\x00$/.test(p) ? p : esc(p)).join('');
  1014|    /* images first — protect them from link regex */
  1015|    const imgs = [];
  1016|    s = s.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, (_, alt, src) => {
  1017|      imgs.push(`<img src="${src.replace(/"/g,'"')}" alt="${alt.replace(/"/g,'"')}" loading="lazy">`);
  1018|      return '\x00i'+(imgs.length-1)+'\x00';
  1019|    });
  1020|    /* inline styles */
  1021|    s = s.replace(/==(.+?)==/g, '$1');
  1022|    s = s.replace(/\*\*(.+?)\*\*/g, '$1');
  1023|    s = s.replace(/\*(.+?)\*/g, '$1');
  1024|    s = s.replace(/\[(.+?)\]\((.+?)\)/g, '$1');
  1025|    s = s.replace(/\x00i(\d+)\x00/g, (_, i) => imgs[+i]);
  1026|    /* legal auto-links */
  1027|    s = applyRisLinks(s);
  1028|    /* restore code spans */
  1029|    s = s.replace(/\x00c(\d+)\x00/g, (_, i) => codes[+i]);
  1030|    return s;
  1031|  };
  1032|
  1033|  /* ── RICH TEXT RENDER ──────────────────────────────── */
  1034|  const ROMAN = ['i','ii','iii','iv','v','vi','vii','viii','ix','x','xi','xii','xiii','xiv','xv'];
  1035|
  1036|  const renderText = raw => {
  1037|    if (_abbrs.length) raw = applyAbbrs(String(raw || ''));
  1038|    const lines = String(raw || '').split('\n');
  1039|    const out = [];
  1040|    let list = null; // null | 'ul' | 'ol' | 'ol-a' | 'ol-i'
  1041|    const closeList = () => { if (list) { out.push(list === 'ul' ? '
' : '
'); list = null; } };
  1042|
  1043|    for (const line of lines) {
  1044|      const t    = line.trim();
  1045|      const pad  = line.length - line.trimStart().length;
  1046|      const ml   = pad > 0 ? ` style="margin-left:${pad * 8}px"` : '';
  1047|
  1048|      /* headings */
  1049|      const hm = t.match(/^(#{1,3})\s+(.*)/);
  1050|      if (hm) { closeList(); out.push(`<h${hm[1].length}>${inlineFmt(hm[2])}</h${hm[1].length}>`); continue; }
  1051|
  1052|      /* horizontal rule */
  1053|      if (/^-{3,}$/.test(t) || /^\*{3,}$/.test(t)) { closeList(); out.push('
'); continue; }
  1054|
  1055|      if (t.startsWith('• ')) {
  1056|        if (list !== 'ul') { closeList(); out.push('
'); list = 'ul'; }
  1057|        out.push(`<li${ml}>${inlineFmt(t.slice(2))}</li>`);
  1058|      } else if (/^\d+\. /.test(t)) {
  1059|        if (list !== 'ol') { closeList(); out.push('
'); list = 'ol'; }
  1060|        out.push(`<li${ml}>${inlineFmt(t.replace(/^\d+\. /,''))}</li>`);
  1061|      } else if (/^(i{1,3}|iv|vi{0,3}|ix)\. /i.test(t)) {
  1062|        if (list !== 'ol-i') { closeList(); out.push('
'); list = 'ol-i'; }
  1063|        out.push(`<li${ml}>${inlineFmt(t.replace(/^[ivx]+\. /i,''))}</li>`);
  1064|      } else if (/^[a-z]\. /.test(t)) {
  1065|        if (list !== 'ol-a') { closeList(); out.push('
'); list = 'ol-a'; }
  1066|        out.push(`<li${ml}>${inlineFmt(t.replace(/^[a-z]\. /,''))}</li>`);
  1067|      } else {
  1068|        closeList();
  1069|        if (t === '') out.push('
');
  1070|        else out.push(inlineFmt(t));
  1071|      }
  1072|    }
  1073|    closeList();
  1074|    return out.join('');
  1075|  };
  1076|
  1077|  /* ── SMART EDITOR ──────────────────────────────────── */
  1078|  const setupSmartEditor = ta => {
  1079|    ta.addEventListener('keydown', e => {
  1080|      const val  = ta.value;
  1081|      const pos  = ta.selectionStart;
  1082|      const ls   = val.lastIndexOf('\n', pos - 1) + 1; // line start
  1083|      const le   = val.indexOf('\n', pos);              // line end
  1084|      const full = val.substring(ls, le === -1 ? val.length : le); // full current line
  1085|      const pre  = val.substring(ls, pos);              // prefix before cursor
  1086|
  1087|      /* ── Tab / Shift+Tab ─────── */
  1088|      if (e.key === 'Tab') {
  1089|        e.preventDefault();
  1090|        if (e.shiftKey) {
  1091|          // Unindent: remove up to 2 leading spaces from line start
  1092|          const sp = full.match(/^( {1,2})/);
  1093|          if (sp) {
  1094|            ta.setSelectionRange(ls, ls + sp[1].length);
  1095|            document.execCommand('insertText', false, '');
  1096|          }
  1097|        } else {
  1098|          const isListLine = /^(\s*)(• |\d+\. |[a-z]\. |[ivx]+\. )/i.test(full);
  1099|          if (isListLine) {
  1100|

Use offset=1100 to continue reading (showing 900-1099 of 2490 lines)


Explanation:
Let me understand the Zeitgeist Anki data structure. From the data.json I saw earlier, the key is `zeitgeist-anki-v2` and it contains `{"decks": {...}}`. The decks have IDs, names, colors. But there are no cards/notes in the current data. Let me look at how the anki.html frontend stores cards and notes.

Let me search for the data model in the anki.html file - specifically how cards and notes are stored.


Explanation:
The search for "cards|notes|deckId|noteId" in anki.html returned 0 results. That's odd - the Anki data structure must be defined differently. Let me look at the data structure more carefully. The data.json has `zeitgeist-anki-v2` which contains `decks` but no `cards` or `notes`. Let me look at the anki.html more carefully to understand the data model.

Let me search for the actual data structure in anki.html - how cards/notes are stored.


Explanation: Let me search for the data structure in the anki.html file more carefully. I need to understand how Zeitgeist stores cards and notes.


Explanation: The search for "cache\[|\.cards|\.notes|\.due|\.interval" returned 0 results. Let me look at the anki.html more carefully to understand the data structure. Let me search for the data model morebroadly.


1100|            // Indent: insert 2 spaces at start of line so "• item" → "  • item"
  1101|            const cur = ta.selectionStart;
  1102|            ta.setSelectionRange(ls, ls);
  1103|            document.execCommand('insertText', false, '  ');
  1104|            ta.setSelectionRange(cur + 2, cur + 2);
  1105|          } else {
  1106|            document.execCommand('insertText', false, '  ');
  1107|          }
  1108|        }
  1109|        return;
  1110|      }
  1111|
  1112|      /* ── Space / Enter: expand abbreviation if cursor follows one ─── */
  1113|      if (e.key === ' ' || e.key === 'Enter') {
  1114|        if (_abbrs.length) {
  1115|          for (const _a of _abbrs) {
  1116|            if (_a.short && _a.long && pre.endsWith(_a.short)) {
  1117|              const start = pos - _a.short.length;
  1118|              ta.setSelectionRange(start, pos);
  1119|              document.execCommand('insertText', false, _a.long);
  1120|              break;
  1121|            }
  1122|          }
  1123|        }
  1124|      }
  1125|
  1126|      /* ── Space: trigger list markers ─── */
  1127|      if (e.key === ' ') {
  1128|        const indent = pre.match(/^(\s*)/)[1];
  1129|        const token  = pre.trimStart();
  1130|        if (token === '-') {
  1131|          e.preventDefault();
  1132|          ta.setSelectionRange(ls + indent.length, pos);
  1133|          document.execCommand('insertText', false, '• ');
  1134|          return;
  1135|        }
  1136|        // 1. / a. / i. — just let space be inserted normally, list is triggered by content
  1137|      }
  1138|
  1139|      /* ── Enter: continue list ─── */
  1140|      if (e.key === 'Enter') {
  1141|        const indent = full.match(/^(\s*)/)[1];
  1142|
  1143|        const bm = full.match(/^(\s*)• (.*)$/);
  1144|        if (bm) {
  1145|          e.preventDefault();
  1146|          if (!bm[2].trim()) { ta.setSelectionRange(ls, ls + indent.length + 2); document.execCommand('insertText', false, ''); }
  1147|          else document.execCommand('insertText', false, '\n' + indent + '• ');
  1148|          return;
  1149|        }
  1150|
  1151|        const nm = full.match(/^(\s*)(\d+)\. (.*)$/);
  1152|        if (nm) {
  1153|          e.preventDefault();
  1154|          if (!nm[3].trim()) { ta.setSelectionRange(ls, ls + indent.length + nm[2].length + 2); document.execCommand('insertText', false, ''); }
  1155|          else document.execCommand('insertText', false, '\n' + indent + (parseInt(nm[2]) + 1) + '. ');
  1156|          return;
  1157|        }
  1158|
  1159|        const rm = full.match(/^(\s*)(i{1,3}|iv|vi{0,3}|ix)\. (.*)$/i);
  1160|        if (rm) {
  1161|          e.preventDefault();
  1162|          const cur = rm[2].toLowerCase(), ri = ROMAN.indexOf(cur);
  1163|          if (!rm[3].trim()) { ta.setSelectionRange(ls, ls + indent.length + cur.length + 2); document.execCommand('insertText', false, ''); }
  1164|          else document.execCommand('insertText', false, '\n' + indent + (ri !== -1 && ri < ROMAN.length - 1 ? ROMAN[ri + 1] : cur + 'i') + '. ');
  1165|          return;
  1166|        }
  1167|
  1168|        const am = full.match(/^(\s*)([a-z])\. (.*)$/);
  1169|        if (am) {
  1170|          e.preventDefault();
  1171|          if (!am[3].trim()) { ta.setSelectionRange(ls, ls + indent.length + 3); document.execCommand('insertText', false, ''); }
  1172|          else document.execCommand('insertText', false, '\n' + indent + String.fromCharCode(am[2].charCodeAt(0) + 1) + '. ');
  1173|          return;
  1174|        }
  1175|      }
  1176|    });
  1177|  };
  1178|  const strip = h => { const d=document.createElement('div'); d.innerHTML=h; return d.textContent||''; };
  1179|
  1180|  const getDeck  = id => load().decks[id];
  1181|  const getCards = id => (getDeck(id)||{}).cards||[];
  1182|  const colorOf  = d  => d.color||COLORS[0];
  1183|
  1184|  const countsDue = (cards) => {
  1185|    const now = Date.now();
  1186|    return {
  1187|      due:   cards.filter(c => c.state!=='new' && c.dueDate && c.dueDate<=now).length,
  1188|      newN:  cards.filter(c => c.state==='new'||!c.state).length,
  1189|    };
  1190|  };
  1191|
  1192|  const newCard = (front, back) => ({
  1193|    id: uid(), front, back, created: Date.now(),
  1194|    state: 'new', interval: 0,
  1195|    stability: null, difficulty: null, lastReview: null,
  1196|    dueDate: null, lapses: 0, reviews: 0, learningStep: 0,
  1197|    /* legacy field, kept for backward read compat — unused by FSRS */
  1198|    easeFactor: 2.5,
  1199|  });
  1200|
  1201|  /* ═══════════════════════════════════════
  1202|     STATE
  1203|     ═══════════════════════════════════════ */
  1204|  let VIEW   = 'decks';
  1205|  let deckId = null;
  1206|  let cardId = null;
  1207|  let learn  = {};
  1208|  let activeCtx = null;
  1209|  let _editFromLearn = false;
  1210|
  1211|  /* Suche/Filter der Kartenliste (pro Deck zurückgesetzt) */
  1212|  let cardQuery  = '';
  1213|  let cardFilter = 'all';
  1214|
  1215|  const go = (v, p={}) => {
  1216|    VIEW=v;
  1217|    if(p.deckId!==undefined) {
  1218|      if (p.deckId !== deckId) { cardQuery = ''; cardFilter = 'all'; }
  1219|      deckId=p.deckId;
  1220|    }
  1221|    if(p.cardId!==undefined) cardId=p.cardId;
  1222|    if(p.learn !==undefined) learn =p.learn;
  1223|    render();
  1224|  };
  1225|
  1226|  /* ═══════════════════════════════════════
  1227|     ICONS
  1228|     ═══════════════════════════════════════ */
  1229|  const IC = {
  1230|    plus:   `<svg viewBox="0 0 16 16"><line x1="8" y1="3" x2="8" y2="13"/><line x1="3" y1="8" x2="13" y2="8"/></svg>`,
  1231|    edit:   `<svg viewBox="0 0 16 16"><path d="M11 2l3 3-9 9H2v-3z"/></svg>`,
  1232|    trash:  `<svg viewBox="0 0 16 16"><polyline points="3,5 13,5"/><path d="M6,5V3h4v2M5,5v8a1,1,0,0,0,1,1h4a1,1,0,0,0,1-1V5"/></svg>`,
  1233|    play:   `<svg viewBox="0 0 16 16"><polygon points="4,2 14,8 4,14"/></svg>`,
  1234|    rotate: `<svg viewBox="0 0 16 16"><path d="M2 8a6 6 0 1 0 1.5-4"/><polyline points="2,2 2,6 6,6"/></svg>`,
  1235|    import: `<svg viewBox="0 0 16 16"><path d="M8 2v9"/><polyline points="4,7 8,11 12,7"/><rect x="2" y="12" width="12" height="2" rx="1"/></svg>`,
  1236|    open:   `<svg viewBox="0 0 16 16"><path d="M2 5h4l1.5 1.5H14V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z"/></svg>`,
  1237|    stats:  `<svg viewBox="0 0 16 16"><line x1="3" y1="13" x2="3" y2="9"/><line x1="8" y1="13" x2="8" y2="5"/><line x1="13" y1="13" x2="13" y2="7"/></svg>`,
  1238|  };
  1239|
  1240|  /* ═══════════════════════════════════════
  1241|     RENDER
  1242|     ═══════════════════════════════════════ */
  1243|  const render = () => {
  1244|    updateHeader();
  1245|    const app = document.getElementById('app');
  1246|    if      (VIEW==='decks')       app.innerHTML = vDecks();
  1247|    else if (VIEW==='cards')       app.innerHTML = vCards();
  1248|    else if (VIEW==='learn')       app.innerHTML = vLearn();
  1249|    else if (VIEW==='editor')      app.innerHTML = vEditor();
  1250|    else if (VIEW==='done')        app.innerHTML = vDone();
  1251|    else if (VIEW==='deck-stats')  app.innerHTML = vDeckStats();
  1252|    bindAll();
  1253|  };
  1254|
  1255|  const updateHeader = () => {};
  1256|
  1257|  /* ── VIEW: DECKS ───────────────────────── */
  1258|  const vDecks = () => {
  1259|    const decks = Object.values(load().decks).sort((a,b)=>a.created-b.created);
  1260|    if (!decks.length) return `
  1261|

  1262|
📚

  1263|
Noch keine Decks

  1264|
Erstelle dein erstes Deck oder importiere ein Anki-Deck.

  1265|

  1266|          <button class="btn btn-primary btn-lg" id="add-deck-btn">${IC.plus} Neues Deck</button>
  1267|          <button class="btn btn-ghost btn-lg" id="import-btn">${IC.import} Importieren</button>
  1268|

  1269|
`;
  1270|
  1271|    const cards = decks.map(d => {
  1272|      const c=colorOf(d); const {due,newN}=countsDue(d.cards||[]);
  1273|      return `
  1274|

  1275|

  1276|
${esc(d.name)}

  1277|

  1278|            ${due  ? `<span class="deck-count"><span class="dot" style="background:#EF4444"></span>${due} fällig</span>` : ''}
  1279|            ${newN ? `<span class="deck-count"><span class="dot" style="background:${c}"></span>${newN} neu</span>` : ''}
  1280|            ${!due&&!newN ? `<span style="color:var(--text-3)">${(d.cards||[]).length} Karten</span>` : ''}
  1281|

  1282|

  1283|            <button class="deck-learn-btn" data-learn="${d.id}" style="background:${c}" ${!(d.cards||[]).length?'disabled':''}>Lernen</button>
  1284|            <button class="deck-menu-btn" data-menu="${d.id}">···</button>
  1285|

  1286|
`;
  1287|    }).join('');
  1288|
  1289|    return `
  1290|

  1291|
Meine Decks

  1292|

  1293|          <button class="btn btn-ghost" id="import-btn">${IC.import} Importieren</button>
  1294|          <button class="btn btn-primary" id="add-deck-btn">${IC.plus} Neues Deck</button>
  1295|

  1296|

  1297|

  1298|        ${cards}
  1299|
${IC.plus} Deck hinzufügen

  1300|
`;
  1301|  };
  1302|
  1303|  /* ── VIEW: DECK STATS ─────────────────── */
  1304|  const vDeckStats = () => {
  1305|    const deck = getDeck(deckId); if (!deck) return '';
  1306|    const cards = deck.cards || [];
  1307|    const c = colorOf(deck);
  1308|    const total = cards.length;
  1309|    const newN = cards.filter(c => !c.state || c.state === 'new').length;
  1310|    const learning = cards.filter(c => c.state === 'learning' || c.state === 'relearn').length;
  1311|    const matured = cards.filter(c => c.state === 'review' && (c.interval || 0) > 21).length;
  1312|
  1313|    // State distribution
  1314|    const reviewing = cards.filter(c => c.state === 'review').length;
  1315|    const relearn = cards.filter(c => c.state === 'relearn').length;
  1316|    const tot = total || 1;
  1317|    const pNew     = Math.round((newN / tot) * 100);
  1318|    const pLearn   = Math.round((learning / tot) * 100);
  1319|    const pReview  = Math.round((reviewing / tot) * 100);
  1320|    const pRelearn = Math.round((relearn / tot) * 100);
  1321|
  1322|    // FSRS stats: average stability (days) + average difficulty (1-10) + leeches
  1323|    const reviewCards = cards.filter(c => c.state === 'review' || c.state === 'relearn');
  1324|    const _stabs = reviewCards.map(c => typeof c.stability === 'number' ? c.stability : (c.interval || 0)).filter(x => x > 0);
  1325|    const _diffs = reviewCards.map(c => typeof c.difficulty === 'number' ? c.difficulty : null).filter(x => x !== null);
  1326|    const avgStab = _stabs.length ? (_stabs.reduce((a,b)=>a+b,0)/_stabs.length) : 0;
  1327|    const avgDiff = _diffs.length ? (_diffs.reduce((a,b)=>a+b,0)/_diffs.length) : 0;
  1328|    const leeches = cards.filter(c => c.leech || (c.lapses || 0) >= FSRS.LEECH_AT).length;
  1329|    const avgStabFmt = avgStab >= 30 ? (avgStab/30).toFixed(1)+'mo' : avgStab.toFixed(1)+'d';
  1330|
  1331|    const stateBar = total ? `
  1332|

  1333|        ${pNew     ? `<div class="ds-seg" style="width:${pNew}%;background:var(--accent)"></div>` : ''}
  1334|        ${pLearn   ? `<div class="ds-seg" style="width:${pLearn}%;background:var(--orange)"></div>` : ''}
  1335|        ${pReview  ? `<div class="ds-seg" style="width:${pReview}%;background:var(--green)"></div>` : ''}
  1336|        ${pRelearn ? `<div class="ds-seg" style="width:${pRelearn}%;background:var(--red)"></div>` : ''}
  1337|

  1338|

  1339|

Neu (${newN})

  1340|

Lernen (${learning})

  1341|

Wiederholen (${reviewing})

  1342|

Relearn (${relearn})

  1343|
` : '
Noch keine Karten
';
  1344|
  1345|    return `
  1346|

  1347|

  1348|
${esc(deck.name)} · Statistik

  1349|

  1350|

  1351|          <button class="btn btn-primary" id="start-learn-btn" style="background:${c}" ${!cards.length?'disabled':''}>
  1352|            ${IC.play} Lernen starten
  1353|          </button>
  1354|

  1355|

  1356|

  1357|

  1358|
${total}

  1359|
Karten gesamt

  1360|

  1361|

  1362|
${newN}

  1363|
Neu

  1364|

  1365|

  1366|
${learning}

  1367|
In Lernphase

  1368|

  1369|

  1370|
${matured}

  1371|
Gereift

  1372|

  1373|

  1374|

  1375|
Zustand

  1376|        ${stateBar}
  1377|

  1378|

  1379|

  1380|
Ø Stabilität (Gedächtnis)

  1381|
${reviewCards.length ? avgStabFmt : '—'}

  1382|

  1383|

  1384|
Ø Schwierigkeit

  1385|
${reviewCards.length ? avgDiff.toFixed(1)+' / 10' : '—'}

  1386|

  1387|

  1388|
Leeches

  1389|
0?'color:var(--red)':''}">${leeches}

  1390|

  1391|
`;
  1392|  };
  1393|
  1394|  /* ── VIEW: CARDS ───────────────────────── */
  1395|  const vCards = () => {
  1396|    const deck  = getDeck(deckId); if(!deck) return '';
  1397|    const cards = deck.cards||[];
  1398|    const c     = colorOf(deck);
  1399|    const {due,newN} = countsDue(cards);
  1400|

Use offset=1400 to continue reading (showing 1100-1399 of 2490 lines)


Jetzt hab ich alles verstanden. Zeitgeist speichert Karten so:

```js
{ id, front, back, created, state: 'new', interval: 0, stability: null, difficulty: null, lastReview: null, dueDate: null, lapses: 0, reviews: 0, learningStep: 0, easeFactor: 2.5 }
```

Ich schreib jetzt ein Script, das die 90 Karten aus der `.apkg` ausliest und direkt ins Zivilrecht-Deck (`mq0izjiyplw9`) einfügt.

Explanation:
Now I understand the Zeitgeist Anki data structure. Let me look at the key parts:

1. **Storage key**: `zeitgeist-anki-v2` in `data.json`
2. **Card structure** (from `newCard` function, line 1192-1199):
```js
{
  id: uid(), front, back, created: Date.now(),
  state: 'new', interval: 0,
  stability: null, difficulty: null, lastReview: null,
  dueDate: null, lapses: 0, reviews: 0, learningStep: 0,
  easeFactor: 2.5,
}
```

3. **Deck structure**: `{decks: {id: {id, name, color, created, cards: [...]}}}`

4. The data is stored as a JSON string inside `data.json` under key `zeitgeist-anki-v2`.

Now I need to:
1. Read the current `data.json`
2. Parse the `zeitgeist-anki-v2` value
3. Add 90 cards to the Zivilrecht deck (`mq0izjiyplw9`)
4. Write back

The source `.apkg` has:
- Deck: "FPS Zivilrecht" (ID 1765743870788)
- 90 notes, each with `flds` = "Front\x1fBack" (separated by ASCII 0x1f)
- Notetype: "Einfach" (ID 1750066365828) - basic front/back

I need to convert each note to a Zeitgeist card:
- `front` = first field (before \x1f)
- `back` = second field (after \x1f)
- `id` = generate new uid
- `state` = 'new'
- All other fields as defaults

Let me write a Python script to do this.

#!/usr/bin/env python3
"""Import FPS Zivilrecht .apkg cards into Zeitgeist Anki."""

import sqlite3
import json
import time
import random
import shutil
import os

# ── Config ──────────────────────────────────────────────
APKG_DB    = '/tmp/apkg_extract/collection_decompressed.anki21'
ZG_DATA    = '/media/HDD_1TB/Zeitgeist/data/data.json'
ZG_BACKUPS = '/media/HDD_1TB/Zeitgeist/data/backups'
TARGET_DECK_ID = 'mq0izjiyplw9'  # Zivilrecht
STORE_KEY  = 'zeitgeist-anki-v2'

# ── Read source cards ───────────────────────────────────
conn = sqlite3.connect(APKG_DB)
cur = conn.cursor()

# Get all notes with their deck assignment
cur.execute("""
    SELECT n.id, n.guid, n.flds, n.tags, c.did
    FROM notes n
    JOIN cards c ON c.nid = n.id
    WHERE c.did = 1765743870788  -- FPS Zivilrecht deck
""")
rows = cur.fetchall()
conn.close()

print(f"Found {len(rows)} notes in FPS Zivilrecht deck")

# ── Generate Zeitgeist cards ────────────────────────────
def uid():
    """Same as Zeitgeist's uid(): Date.now().toString(36) + random 4 chars"""
    t = int(time.time() * 1000)
    b36 = ''
    chars = '0123456789abcdefghijklmnopqrstuvwxyz'
    while t > 0:
        b36 = chars[t % 36] + b36
        t //= 36
    rand = ''.join(random.choice(chars) for _ in range(4))
    return b36 + rand

new_cards = []
for row in rows:
    note_id, guid, flds, tags, did = row
    # flds is "Front\x1fBack"
    parts = flds.split('\x1f')
    front = parts[0] if len(parts) > 0 else ''
    back = parts[1] if len(parts) > 1 else ''

    card = {
        'id': uid(),
        'front': front,
        'back': back,
        'created': int(time.time() * 1000),
        'state': 'new',
        'interval': 0,
        'stability': None,
        'difficulty': None,
        'lastReview': None,
        'dueDate': None,
        'lapses': 0,
        'reviews': 0,
        'learningStep': 0,
        'easeFactor': 2.5,
    }
    new_cards.append(card)

print(f"Generated {len(new_cards)} Zeitgeist cards")

# ── Backup current data.json ─────────────────────────────
ts = time.strftime('%Y-%m-%d_%H%M%S')
backup_path = os.path.join(ZG_BACKUPS, f'pre-anki-import-{ts}.json')
shutil.copy2(ZG_DATA, backup_path)
print(f"Backup saved: {backup_path}")

# ── Read & update Zeitgeist data ────────────────────────
with open(ZG_DATA, 'r', encoding='utf-8') as f:
    data = json.load(f)

anki_raw = data.get(STORE_KEY, '{"decks":{}}')
anki = json.loads(anki_raw)

# Ensure Zivilrecht deck exists
if TARGET_DECK_ID not in anki.get('decks', {}):
    print(f"ERROR: Deck {TARGET_DECK_ID} not found!")
    exit(1)

deck = anki['decks'][TARGET_DECK_ID]
existing = deck.get('cards', [])
print(f"Deck '{deck['name']}' had {len(existing)} cards before import")

# Append new cards
deck['cards'] = existing + new_cards
print(f"Deck now has {len(deck['cards'])} cards")

# Write back
data[STORE_KEY] = json.dumps(anki, ensure_ascii=False)

# Atomic write (same pattern as Zeitgeist server.js)
tmp_path = ZG_DATA + '.tmp'
with open(tmp_path, 'w', encoding='utf-8') as f:
    json.dump(data, f, ensure_ascii=False)
os.replace(tmp_path, ZG_DATA)

print(f"\n✅ Import complete: {len(new_cards)} cards added to '{deck['name']}'")
print(f"   Backup: {backup_path}")
print(f"   Total cards in deck: {len(deck['cards'])}")

# ── Show sample ─────────────────────────────────────────
print("\n── Sample cards ──")
for c in new_cards[:3]:
    print(f"  Front: {c['front'][:80]}")
    print(f"  Back:  {c['back'][:80]}")
    print()


Explanation:
Das Script ist geschrieben. Jetzt führe ich es aus. Aber vorher muss ich sicherstellen, dass der Zeitgeist-Container läuft und die Daten korrekt sind. Das Script macht ein Backup vor dem Import, das ist gut.

Lass es laufen.


terminal

Output:
Found 90 notes in FPS Zivilrecht deck
Generated 90 Zeitgeist cards
Traceback (most recent call last):
  File "/tmp/import_zivilrecht.py", line 77, in <module>
    shutil.copy2(ZG_DATA, backup_path)
  File "/DATA/AppData/.local/share/uv/python/cpython-3.11.15-linux-x86_64-gnu/lib/python3.11/shutil.py", line 448, in copy2
    copyfile(src, dst, follow_symlinks=follow_symlinks)
  File "/DATA/AppData/.local/share/uv/python/cpython-3.11.15-linux-x86_64-gnu/lib/python3.11/shutil.py", line 258, in copyfile
    with open(dst, 'wb') as fdst:
         ^^^^^^^^^^^^^^^
PermissionError: [Errno 13] Permission denied: '/media/HDD_1TB/Zeitgeist/data/backups/pre-anki-import-2026-06-11_114307.json'


Explanation: The backup directory is owned by root (from the earlier ls output: `drwxr-xr-x 1 root root 1482 Jun 11 11:33 backups`). I'm running as `az-a` user. Let me check and fix permissions, or just skip the backup and write directly.

whoami && ls -la /media/HDD_1TB/Zeitgeist/data/backups/ | head -3 && ls -la /media/HDD_1TB/Zeitgeist/data/data.json


Output:
az-a
total 4252
drwxr-xr-x 1 root root   1482 Jun 11 11:42 .
drwxr-xr-x 1 root root     46 Jun 11 11:42 ..
-rw-r--r-- 1 root root 110355 Jun 11 11:42 /media/HDD_1TB/Zeitgeist/data/data.json


Explanation: The backups directory is owned by root, so I can't write there as az-a. I'll skip the backup to that directory and instead make a backup to /tmp, then use sudo to write the data.json file.

#!/usr/bin/env python3
"""Import FPS Zivilrecht .apkg cards into Zeitgeist Anki."""

import sqlite3
import json
import time
import random
import shutil
import os

# ── Config ──────────────────────────────────────────────
APKG_DB    = '/tmp/apkg_extract/collection_decompressed.anki21'
ZG_DATA    = '/media/HDD_1TB/Zeitgeist/data/data.json'
TARGET_DECK_ID = 'mq0izjiyplw9'  # Zivilrecht
STORE_KEY  = 'zeitgeist-anki-v2'

# ── Read source cards ───────────────────────────────────
conn = sqlite3.connect(APKG_DB)
cur = conn.cursor()

cur.execute("""
    SELECT n.id, n.guid, n.flds, n.tags, c.did
    FROM notes n
    JOIN cards c ON c.nid = n.id
    WHERE c.did = 1765743870788
""")
rows = cur.fetchall()
conn.close()

print(f"Found {len(rows)} notes in FPS Zivilrecht deck")

# ── Generate Zeitgeist cards ────────────────────────────
def uid():
    t = int(time.time() * 1000)
    b36 = ''
    chars = '0123456789abcdefghijklmnopqrstuvwxyz'
    while t > 0:
        b36 = chars[t % 36] + b36
        t //= 36
    rand = ''.join(random.choice(chars) for _ in range(4))
    return b36 + rand

new_cards = []
for row in rows:
    note_id, guid, flds, tags, did = row
    parts = flds.split('\x1f')
    front = parts[0] if len(parts) > 0 else ''
    back = parts[1] if len(parts) > 1 else ''

    card = {
        'id': uid(),
        'front': front,
        'back': back,
        'created': int(time.time() * 1000),
        'state': 'new',
        'interval': 0,
        'stability': None,
        'difficulty': None,
        'lastReview': None,
        'dueDate': None,
        'lapses': 0,
        'reviews': 0,
        'learningStep': 0,
        'easeFactor': 2.5,
    }
    new_cards.append(card)

print(f"Generated {len(new_cards)} Zeitgeist cards")

# ── Backup to /tmp ──────────────────────────────────────
ts = time.strftime('%Y-%m-%d_%H%M%S')
backup_path = f'/tmp/zg-backup-pre-anki-{ts}.json'
shutil.copy2(ZG_DATA, backup_path)
print(f"Backup saved: {backup_path}")

# ── Read & update Zeitgeist data ────────────────────────
with open(ZG_DATA, 'r', encoding='utf-8') as f:
    data = json.load(f)

anki_raw = data.get(STORE_KEY, '{"decks":{}}')
anki = json.loads(anki_raw)

if TARGET_DECK_ID not in anki.get('decks', {}):
    print(f"ERROR: Deck {TARGET_DECK_ID} not found!")
    exit(1)

deck = anki['decks'][TARGET_DECK_ID]
existing = deck.get('cards', [])
print(f"Deck '{deck['name']}' had {len(existing)} cards before import")

deck['cards'] = existing + new_cards
print(f"Deck now has {len(deck['cards'])} cards")

data[STORE_KEY] = json.dumps(anki, ensure_ascii=False)

# Write to tmp, then move with sudo
tmp_path = '/tmp/zg-data-new.json'
with open(tmp_path, 'w', encoding='utf-8') as f:
    json.dump(data, f, ensure_ascii=False)

print(f"New data.json written to {tmp_path}")
print(f"Now run: sudo mv {tmp_path} {ZG_DATA}")
print(f"\n✅ {len(new_cards)} cards ready for import into '{deck['name']}'")

# ── Show sample ──
print("\n── Sample cards ──")
for c in new_cards[:3]:
    print(f"  Front: {c['front'][:80]}")
    print(f"  Back:  {c['back'][:80]}")
    print()


Explanation: The script ran successfully. Now I need to move the new data.json into place with sudo, since the file is owned by root.


Output:
Found 90 notes in FPS Zivilrecht deck
Generated 90 Zeitgeist cards
Backup saved: /tmp/zg-backup-pre-anki-2026-06-11_114340.json
Deck 'Zivilrecht' had 1 cards before import
Deck now has 91 cards
New data.json written to /tmp/zg-data-new.json
Now run: sudo mv /tmp/zg-data-new.json /media/HDD_1TB/Zeitgeist/data/data.json

✅ 90 cards ready for import into 'Zivilrecht'

── Sample cards ──
  Front: Welche Formvorschrift gilt für die Bürgschaft?
  Back:  Schriftformgebot

  Front: Was folgt aus der Akzessorietät der Bürgschaft für Einwendungen des Bürgen?
  Back:  Der Bürge kann grundsätzlich alle Einwendungen geltend machen, die auch dem Haup

  Front: Auf welcher Rechtsgrundlage geht die Forderung des Gläubigers auf den Bürgen übe
  Back:  Legalzession gemäß § 1358 ABGB


Explanation: Wait, there was already 1 card in the Zivilrecht deck? That's odd — earlier the check showed 0 cards and 0 notes. Let me not worry about that. The import generated 90 cards, total 91. Now I need to move the file with sudo.

sudo mv /tmp/zg-data-new.json /media/HDD_1TB/Zeitgeist/data/data.json && echo "OK: data.json replaced" && ls -la /media/HDD_1TB/Zeitgeist/data/data.json


Output:
OK: data.json replaced
-rw-r--r-- 1 az-a samba 160707 Jun 11 11:43 /media/HDD_1TB/Zeitgeist/data/data.json


Explanation:
Wait, the file is now owned by az-a:samba, but the original was root:root. Let me check if Zeitgeist (running as root in Docker) can still read it. The container runs as root, and the file is world-readable (644), so it should be fine. But let me verify the import actually worked by reading back the data.

Also, I noticed the deck had 1 card before import — that's odd, earlier it showed 0 cards. Let me check what that card is.

/DATA/AppData/hermes/venv/bin/python << 'PYEOF'
import json

with open('/media/HDD_1TB/Zeitgeist/data/data.json', 'r') as f:
    data = json.load(f)

anki = json.loads(data['zeitgeist-anki-v2'])
deck = anki['decks']['mq0izjiyplw9']
cards = deck['cards']

print(f"Deck: {deck['name']}")
print(f"Total cards: {len(cards)}")

# Check the 1 pre-existing card
for c in cards:
    if c['front'] not in ['Welche Formvorschrift gilt für die Bürgschaft?', 'Was folgt aus der Akzessorietät der Bürgschaft für Einwendungen des Bürgen?', 'Auf welcher Rechtsgrundlage geht die Forderung des Gläubigers auf den Bürgen über, wenn dieser zahlt?']:
        print(f"\nPre-existing card: {c['front'][:100]} → {c['back'][:100]}")

# Count new vs old
new_count = sum(1 for c in cards if c.get('state') == 'new' and c.get('reviews', 0) == 0)
print(f"New cards (state=new, reviews=0): {new_count}")
print(f"Other cards: {len(cards) - new_count}")
PYEOF


Output:
Deck: Zivilrecht
Total cards: 91

Pre-existing card: Umfang des Pfandrechts gem § 451 ABGB → Dispositive Bestimmung: Im Zweifel gilt jedes Zubehör der Hauptsache als mitverpfändet.
• Zubehör mu

Pre-existing card: Welche Rechtsfolge tritt ein, wenn der Bürge zahlt, ohne sich mit dem Hauptschuldner zu verständigen → Der Hauptschuldner kann dem Bürgen alle Einwendungen entgegenhalten, die er auch gegen den Gläubiger

Pre-existing card: Pfandrechtserwerb →

Titel (Pfandbestellungsvertrag oder Gesetz)

Dingliche Berechtigung des Vormanns<

Pre-existing card: Spezialitätsprinzip im Pfandrecht → Pfandrecht kann nur an einzelnen, konkret bestimmten Sachen bestehen, nicht am gesamten Vermögen

Pre-existing card: Welche Sachen bei einer Hypothek grundsätzlich mitverpfändet? → Alles, was typischerweise zur Liegenschaft gehört, sofern kein Eigentumsvermerk im Grundbuch besteht

Pre-existing card: Gutgläubiger Pfandrechtserwerb → Bei fehlender dinglicher Berechtigung des Verpfänders möglich bei

Gültiger Pfandbestellun

Pre-existing card: Inwiefern ist die Akzessorietät bei der Hypothek gelockert → Hypothek bleibt formell bestehen, bis sie im Grundbuch gelöscht wird, auch wenn die Forderung getilg

Pre-existing card: Was ist eine Simultanhypothek → Hypothek, bei der mehrere Liegenschaften für eine einzige Forderung haften

Pre-existing card: Welches Recht hat der Gläubiger bei der Simultanhypothek → Wahlrecht, aus welcher Liegenschaft er sich befriedigt

Pre-existing card: Höchstbetragshypothek → Deckung einer Forderung nur bis zu einem bestimmten Höchstbetrag

Pre-existing card: Aufklärungspflichten gegenüber Bürgen → § 25c KSchG: Ist Bürge Konsument, muss Unternehmer gesondert über die Bonität des Schuldners aufklär

Pre-existing card: Grundprinzip der EuGVVO für die internationale Zuständigkeit → Actor sequitur forum rei: Kläger muss am Wohnsitz des Beklagten klagen (beachte Wahlge

Pre-existing card: Wann ist VGG anwendbar? →
Verträge zwischen Unternehmer und Verbraucher über Waren (bewegliche körperl

Pre-existing card: Zentrale Unterschiede VGG vs. ABGB →

Vermutungsfrist: 1 Jahr (statt 6 Monate)

Aktualisierung

Pre-existing card: Schadenersatz statt Gewährleistung →
§ 933a ABGB

Wenn Verkäufer Verschulden trifft

Vermutung des Verschulde

Pre-existing card: Beachtlichkeit vom Motivirrtum → 1. Listige Verursachung
2. Motiv ist Bediengung für Kauf
3. Bei letztwilligen Verfügungen
4

Pre-existing card: Voraussetzungen des Irrtums →

Fehlvorstellung

Kausalität

Alternativvoraussetzungen des § 871 Abs 1 ABGB<br

Pre-existing card: Gutgläubiger Erwerb → § 367 ABGB
Bewegliche Sache, die gutgläubig

gegen Entgelt in einer öffentlichen

Pre-existing card: Schadenersatz Voraussetzungen § 1295 ABGB → 1. Schaden
2. Kausalität
   - Aquivalenz
   - Adäqunaz
3. Rech

Pre-existing card: Garantie nach § 880a ABGB → Haftung des Garanten gegenüber dem Begünstigten für einen bestimmten Erfolg; tritt dieser nicht ein,

Pre-existing card: Sachverständigenhaftung gem § 1299 ABGB → Führt gegenüber dem allgemeinen Sorgfaltsmaßstab, der für alle

Pre-existing card: Welche rechtlichen Kerneigenschaften hat die (Bank-)Garantie? →

Abstrakt (keine Akzessorietät),

Schriftform analog § 1346 Abs 2 ABGB,

Einwen

Pre-existing card: Hauptpflichten des Beauftragten beim Auftrag (§ 1002ff ABGB) →

Sorgfalt

Weisungseinholung

Auswahlverschulden bei Subauftrag

Herausg

Pre-existing card: Hauptpflichten des Auftraggebers beim Auftrag (§ 1002ff ABGB) →

Entgelt sowie Ersatz/Vorschuss für Barauslagen (§ 1014 ABGB)

Schadenersatzpflichten<

Pre-existing card: Schuldnerverzug – Definition → Nichtleistung zu gehöriger Zeit, am gehörigen Ort oder auf gehörige Weise (§ 918 ABGB).

Pre-existing card: Objektiver Schuldnerverzug – Rechtsfolgen → Wahlrecht des Gläubigers zwischen Erfüllung oder Rücktritt; Rücktritt grundsätzlich erst nach angeme

Pre-existing card: Subjektiver Schuldnerverzug → Verzug mit Verschulden → Schadenersatzansprüche (Erfüllungsinteresse)

Pre-existing card: PHG – ersatzfähiger Schaden →

Personenschäden,

Schäden an anderen körperlichen Sachen, beschränkt auf Wiederherste

Pre-existing card: PHG – Haftungsvoraussetzungen →

Schaden muss durch Produktfehler verursacht sein

Produkt muss in Verkehr gebrach

Pre-existing card: PHG – Haftungsausschluss / Verjährung →

Wenn Produkt bei Inverkehrbringung nach Stand der Technik fehlerfrei war

Haftungsgre

Pre-existing card: Versendungskauf an Konsumenten → § 7 KSchG: Unternehmer trägt das Risiko, bis Ware beim Verbraucher ankommt. Erst dann geht Gefa

Pre-existing card: EKHG Anwendungsbereich →

Anwendung bei Tod, Körperverletzung oder Sachbeschädigung durch Betrieb von Kfz oder Eisenba

Pre-existing card: EKHG Prüfschema →

Anwendungsbereich

Haftungsbefreiung

Vorliegen einer außergewöhnliche Betrieb

Pre-existing card: FAGG → Gilt für Fernabsatzgeschäfte (Telefon, E-Mail, Internet) und außerhalb von Geschäftsräumen geschloss

Pre-existing card: Voraussetungen der Stellvertretung →

Offenlegungsgrundsatz

(Zumindest beschränkte) Geschäftsfähigkeit

Hinreichend

Pre-existing card: Strenges Publizitätsprinzip im Pfandrecht → - Ware MUSS übergeben werden, Pfandbesteller darf Sache nicht innehaben!
- Auch wenn Ware “h

Pre-existing card: Stellung des Pfandgläubigers → Vor Fälligkeit
- Sorgfältige Verwahrung der Pfandsache
- Darf keine Früchte ziehen
- D

Pre-existing card: Hypotheken § 928 → Schulden und Rückstände welche auf der Sache lasten, müssen stets vom Übergeber übernommen werden.

Pre-existing card: Vorgehen bei unzureichendem Pfand → § 458 ABGB
- Gewährleistung (wenn Mangel bei Übergabe vorliegt)
- Schadenersatz (verschuldensu

Pre-existing card: Ratgeberhaftung → § 1300 Satz 2:
Haftung, wenn Ratgeber wissentlich durch Erteilung eines Rates jemandem einen Scha

Pre-existing card: Was wird mitverpfändet? → § 457 ABGB (dispositiv)
- Zubehörstücke
   - Dienen ihrer objektiven Zweckbest

Pre-existing card: Tiermängel (Gewährleistung) → - Gem § 925 ABGB TiermängelVO beachtlich
- Bestimmte Fristen für Vermutung (zB Schafräude sieben

Pre-existing card: Unbeweglichkeitsfiktion § 293 Satz 2 ABGB → Unbewegliche Sachen, die Zubehör/Zugehör zu einer unbeweglichen Sache sind, gelten ebenfalls als unb

Pre-existing card: Selbstverbesserung → Wenn Verbesserung (2. Chance) vereitelt wird, sind auch sekundäre GWL-Behelfe nicht mehr

Pre-existing card: Trauerschmerzengeld → - Für nahe Anngehörige
- Setzt grobes Verschulden beim Schädiger voraus
- Prüfung wie § 1295 A

Pre-existing card: Auflösung des Verbraucherkreditvertrags → - Rücktrittsrecht gem § 12 VKrG
- Verbundener Kreditvertrag gem § 13 VKrG (Einwendungsd

Pre-existing card: Rückgriffsanspruch des Bürgen →

Legalzession aufgrund § 1358 ABGB

Regress durch Vereinbarung zwischen Gläubiger un

Pre-existing card: Besondere Aufklärungspflichten bei Bürgschaft → Ist Bürge Konsument im Sinne des KSchG, ist er gem § 25c KSchG besonders Schutzbedürftig und muss üb

Pre-existing card: Absatzfinanzierung →

Verkäufer wird Kredit gewährt

Bank bekommt Kaufpreisforderung abgetreten

Käu

Pre-existing card: Sicherungsübereignung →
= Eigentum an fremder Sache soll bis zur vollständigen Bezahlung der Schuld an Gläubiger gehen<

Pre-existing card: Sicherungszession → = Volle Rechtsübertragung an Gläubiger mit bloßer Bindung nach innen
- Verlangt Publizität a

Pre-existing card: Eigentumsvorbehalt → = Übereignung einer Sache bloß unter der aufschiebenden Bedingung der vollständigen Kaufpreiszahlung

Pre-existing card: Rechtsstellung des Vorbehaltskäufers →

Recht auf Innehabung und Gebrauch sowie Fruchtziehung

Prozessorischer Schutz + “An

Pre-existing card: Übertragung des vorbehaltenen Eigentums →

Übergabe muss durch Besitzanweisung erfolgen

Es kann nur “auflösend bedingtes Eige

Pre-existing card: Verlängerter Eigentumsvorbehalt →
Oft möchte der Käufer die unter <a href="https://www.re

Pre-existing card: Ungültige Verfallsklausel im Pfandrecht →

§ 1371:
Alle dem Wesen des Pfandvertrags widersprechenden Vereinbar

Pre-existing card: IPRG Sachenrecht → - Rechtswahl ausgeschlossen
- Anwendbr sind §§ 31- 33a IPRG: Beurteilung immer nach dem Recht des

Pre-existing card: Was sind Servitute und Reallsten →
= Beschränkte dingliche Nutzungsrechte an fremder Sache

Absolut geschützte Recht

Pre-existing card: Erlöschen von Servituten bei Nichtausübung → - 30-jährige Verjährungsfrist bei nat. Personen
- 40-jährigen Verjährungsfrist bei jur. Personen

Pre-existing card: Entstehen und Erlöschen von Servituten → - Titel (Servitutsbestellungsvertrag) + Modus (Einverleibung im Grundbuch)
- <

Pre-existing card: Freiheitsersitzung → Beeinträchtigt der Eigentümer ds Servitutsrecht und kann diese 3 Jahre in Folge nicht genutzt werden

Pre-existing card: Haftungsbefreiung EKHG → § 9 EKHG:
Unabwendbares Ereignis, das weder auf Fehler in Beschaffenheit noch auf Versagen der Ve

Pre-existing card: Regiepreis → Bezahl wird Entgelt pro Einheit - je länger Arbeiten dauern, desto teurer wird es. Keine Anzeigepfli

Pre-existing card: PHG - Wer haftet? → 1. Hersteller
2. Importeur (wenn er es in EWR einführt)
3. Händler

Pre-existing card: Rücktrittrechte § 7c KSchG → - Schuldnerverug (nicht gehörig erbracht)
- Nachfrist (außer Fixgeschäft)
- Rücktritt wegen sc

Pre-existing card: Schadenersatz Grundlagen → - Schaden
- Kausalität (Aquivalenz und Adäquanz)
- Rechtswidrigkeit (ex contractu/delicto) und

Pre-existing card: Anweisung Valutaverhältnis →
Anweisender → Anweisungsempfänger

Anweisender wird mangels Übergabe nie Ei

Pre-existing card: Anweisung Deckungsverhältnis → Anweisender → Angewiesener

Fehler genau so zu

Pre-existing card: Leasing →
= Finanzierung mit bloßer Gebrauchsüberlassung

Gefahrtragung und Gewährleistung

Pre-existing card: Vertrauensschaden § 1019 →
= Schaden, der dadurch entsteht, dass man auf das Zustandekommen eines Geschäftes erleidet. Ent

Pre-existing card: Haftung eines Sachverständigen gegenüber Dritten → § 1300 analog: Objektiv-Rechtliche Sorgfaltspflicht
- Dritter soll geschützt sein, wenn Sachverst

Pre-existing card: Warnpflicht des Werkunternehmers →
§ 1168a

- Spährentheorie

- Bei Unterlassung hat Werkbesteller Gewährleistun

Pre-existing card: Kündigung von Kreditvertrag gem § 987 → Bei wiederholter Nichteinhaltung der Zahlungsverpflichtung möglich und wesentlicher Verschlechterung

Pre-existing card: Entstehung eines Vertrags → § 869: essentialia negotii + Bindungswillen (besteh iZw so lange nicht, wie noch irgendein Verhandlu

Pre-existing card: Sind Finanzdienstleistungen vom FAGG erfasst? → Nein

Pre-existing card: Haftung des Zedenten → § 1397: Bei entgeltlicher Abtretung haftet der Zedent sowohl für die Richtigkeit, als für die Einbri

Pre-existing card: Falsus Procurator → § 1019: Haftung für Vertrauensschäden

Pre-existing card: Haager Straßenverkehrsübereinkommen → Es gilt das Recht des Unfallorts

Pre-existing card: Vorgehen des Garanten gegen Garantieauftraggeber → 1. Regress gem §§ 1014/1358 analog
2. Rückzahlung der Garantiesumme gem § 1431 analog

Pre-existing card: Rechtmäßiges Alternativverhalten → Schadenersatzentfällt, wenn Erfolg zwar durch rechtswidriges Verhalten eingetreten ist, aber auch be

Pre-existing card: Vertragsübernahme → Bedarf Zustimmung aller beteiligten Parteien

Pre-existing card: Durchbruch der Akzesorietät beim Pfandrecht → § 1352: Bei Verbürgung für Person, die sich nicht verpflichten kann (wenn Bürgen bekannt)

Pre-existing card: Haftung aus cic → - Unmündige haften nicht
- Mündige Minderjährige können haften wenn sie notwendige Einsichtsfähi

Pre-existing card: Luxemburger Vereinbarung → Veto-Recht eines MS bei Beeinträchtigung sehr wichtiger staatlicher Interessen - Pflicht zur Weiterv

Pre-existing card: Gesetzgebung des EU-Parlaments →

Gemeinsam mit Rat

Kein eigenes Initiativrecht

Wahl KOM-Präs auch Vorschlag d

Pre-existing card: EZB →

Sicherung der Preisstabilität

Eigene Rechtspersönlichkeit

Anhörungsrecht</li

Pre-existing card: Entlassung einzelner Kommissionsmitglieder → Nur durch Präsident der Kommission gem Art 17 Abs 6 EUV.  Parlament kann Präsident dazu aufford

Pre-existing card: Rolle der nat. Parlamente →

Kontrolle, Information und demokratische Rückbindung

Frühwarnsystem zur Subsidia
New cards (state=new, reviews=0): 91
Other cards: 0


Explanation:
Wait, the output shows 91 cards total, all with state=new and reviews=0. But earlier the deck had 1 card before import. Let me check — the script said "Deck 'Zivilrecht' had 1 cards before import" and then "Deck now has 91 cards". So 90 were added. But the verification shows 91 new cards. That 1 pre-existing card must have also been state=new. That's fine — all 91 are now in the deck.

But wait — looking at the output more carefully, the "Pre-existing card" section shows many cards that look like they're from the import (Bürgschaft, Pfandrecht, etc.). The script only printed cards whose front didn't match the first 3 sample cards. That's most of them. So the import worked.

But I notice something: the output shows 91 cards total, and the script said 1 pre-existing + 90 new = 91. That checks out. The pre-existing card was "Umfang des Pfandrechts gem § 451 ABGB" — that was already there.

Now I should clean up and confirm to the user. Let me also verify the data.json is valid and the server can read it.

Actually, I should also restart the Zeitgeist container to make sure it picks up the changes, or at least verify the server can still read the file. Let me check if the server is running and if the API still works.

curl -s http://172.29.0.1:8765/zeitgeist.html | head -5 && echo "---SERVER OK---" && /DATA/AppData/hermes/venv/bin/python -c "
import json
with open('/media/HDD_1TB/Zeitgeist/data/data.json') as f:
    data = json.load(f)
anki = json.loads(data['zeitgeist-anki-v2'])
deck = anki['decks']['mq0izjiyplw9']
print(f'Deck: {deck[\"name\"]}')
print(f'Cards: {len(deck[\"cards\"])}')
print('JSON valid ✓')
"


Output:
<!DOCTYPE html>
<html lang="de">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="Cache-Control" content="no-store">
---SERVER OK---
Deck: Zivilrecht
Cards: 91
JSON valid ✓


Erledigt, Chef. **90 Karten** aus FPS Zivilrecht sind jetzt in Zeitgeist.

- **Deck:** Zivilrecht (grün `#10B981`)
- **Vorher:** 1 Karte → **Jetzt:** 91 Karten
- **Status:** Alle neu (`state: new`), bereit zum Lernen
- **Backup:** `/tmp/zg-backup-pre-anki-2026-06-11_114340.json`

Geh auf `http://172.29.0.1:8765/anki.html` und öffne das Zivilrecht-Deck — die Karten sind da. Viel Erfolg beim Lernen!

Explanation: Alles erledigt. 90 Karten aus der FPS Zivilrecht .apkg wurden erfolgreich ins Zeitgeist Zivilrecht-Deck importiert. Jetzt 91 Karten insgesamt (1 war vorher schon drin). Server läuft, JSON valide. Backup liegt in /tmp. Kurze Zusammenfassung für denChef.


Ja aber irgendwie sind die ganzen html befehle auch mit dabei, zb

Anweisung Deckungsverhältnis

Das ist natürlich blödsinn, kannst du das alles bitte säubern?


Stimmt, die HTML-Tags müssen raus. Ich schreib einen Cleaner, der `<span>`, `<div>`, `<b>`, `<i>`, `<br>`, ` ` etc. in sauberes Markdown umwandelt, das Zeitgeists Renderer versteht.

Explanation:
Der Chef bemängelt, dass die importierten Karten HTML-Tags enthalten wie ` `, `<span style="...">`, `<div>`, `<ul>`, `<li>`, `<br>`, `<strong>`, `<b>`, `<i>`, `<em>`, `<a href="...">`, `<h3>`, `<ol>`, `<img>` etc. Das ist tatsächlich blöd — die Karten sollten cleanen Text haben, den Zeitgeist dann selbst mit seinem Markdown-Renderer formatiert.

Ich muss alle 91 Karten durchgehen und die HTML-Tags entfernen bzw. in Markdown-Äquivalente konvertieren, die Zeitgeists `renderText()`-Funktion versteht.

Zeitgeists `renderText()` unterstützt:
- `**bold**` → `<strong>`
- `*italic*` → `<em>`
- `==highlight==` → `<mark>`
- `[text](url)` → `<a>`
- `![alt](url)` → `<img>`
- `# Heading`, `## Heading`, `### Heading`
- `• bullet` → `<ul><li>`
- `1. numbered` → `<ol><li>`
- `i. roman` → `<ol type="i">`
- `a. alpha` → `<ol type="a">`
- `---` or `***` → `<hr>`
- Inline code: `` `code` ``
- §/§§ Auto-Links zu RIS

Also ich muss:
1. `<b>`, `<strong>` → `**text**`
2. `<i>`, `<em>` → `*text*`
3. `<br>` → newline
4. `<ul><li>...</li></ul>` → `• ...`
5. `<ol><li>...</li></ol>` → `1. ...`
6. `<div>...</div>` → just text content
7. `<span style="...">...</span>` → just text content
8. ` ` → space
9. `&` → `&`
10. `<` → `<`
11. `>` → `>`
12. `"` → `"`
13. `<a href="...">text</a>` → `[text](url)` (keep links)
14. `<h3>`, `<h1>`, `<h2>` → `### `, `# `, `## `
15. `<img>` → `![alt](src)`

Ich schreib ein Python-Script, das alle Karten cleaned. Am besten mit BeautifulSoup oder einfach regex-basiert. Da BeautifulSoup wahrscheinlich nicht installiert ist, mach ich's mit regex.

Wichtig: Die Karten sind in `data.json` als JSON-String innerhalb eines JSON-Objekts. Ich muss:
1. data.json lesen
2. `zeitgeist-anki-v2` parsen
3. Für jede Karte im Zivilrecht-Deck: `front` und `back` cleanen
4. Zurückschreiben

Ich sollte auch nur die Karten cleanen, die HTML enthalten — aber eigentlich kann ich alle durchgehen lassen, der Cleaner sollte idempotent sein.

Los geht's.

#!/usr/bin/env python3
"""Clean HTML tags from Zeitgeist Anki cards, converting to Markdown."""

import json
import re
import shutil
import time

ZG_DATA = '/media/HDD_1TB/Zeitgeist/data/data.json'
TARGET_DECK_ID = 'mq0izjiyplw9'
STORE_KEY = 'zeitgeist-anki-v2'

# ── HTML → Markdown cleaner ─────────────────────────────
def clean_html(text):
    """Strip HTML tags and convert to Zeitgeist-compatible Markdown."""
    if not text:
        return text

    s = text

    # 1. Decode HTML entities
    s = s.replace(' ', ' ')
    s = s.replace('&', '&')
    s = s.replace('<', '<')
    s = s.replace('>', '>')
    s = s.replace('"', '"')

    # 2. Convert
 and
 to newlines
    s = re.sub(r'
', '\n', s)

    # 3. Convert  and  to **...**
    s = re.sub(r'</?strong>', '**', s)
    s = re.sub(r'</?b>', '**', s)

    # 4. Convert  and  to *...*
    s = re.sub(r'</?em>', '*', s)
    s = re.sub(r'</?i>', '*', s)

    # 5. Convert text to [text](url)
    s = re.sub(r']*>(.*?)', r'[\2](\1)', s)

    # 6. Convert
,
,
 to markdown headings
    s = re.sub(r'
]*>(.*?)
', r'# \1', s)
    s = re.sub(r'
]*>(.*?)
', r'## \1', s)
    s = re.sub(r'
]*>(.*?)
', r'### \1', s)

    # 7. Convert
...
 blocks: extract
 items
    def convert_ul(match):
        content = match.group(1)
        items = re.findall(r'
]*>(.*?)
', content, re.DOTALL)
        return '\n'.join('• ' + item.strip() for item in items)
    s = re.sub(r'
]*>(.*?)
', convert_ul, s, flags=re.DOTALL)

    # 8. Convert
...
 blocks: extract
 items, number them
    def convert_ol(match):
        content = match.group(1)
        items = re.findall(r'
]*>(.*?)
', content, re.DOTALL)
        return '\n'.join(f'{i+1}. {item.strip()}' for i, item in enumerate(items))
    s = re.sub(r'
]*>(.*?)
', convert_ol, s, flags=re.DOTALL)

    # 9. Strip remaining tags:
, ,
, , , , ,
, etc.
    #    Keep their inner content
    s = re.sub(r'</?(?:div|span|p|mark|u|code|pre|img|hr|table|tr|td|th|thead|tbody|section|article|header|footer|nav|aside|main|figure|figcaption|blockquote|sub|sup|small|s|del|ins|abbr|cite|q|time|var|kbd|samp|wbr|ruby|rt|rp|bdi|bdo|data|datalist|details|dialog|embed|fieldset|form|label|legend|meter|object|optgroup|option|output|param|picture|progress|select|slot|source|summary|template|textarea|track|video|audio|canvas|iframe|map|area|col|colgroup|caption|style|script|noscript|link|meta|base|title|head|body|html)[^>]*>', '', s)

    # 10. Collapse multiple newlines (max 2)
    s = re.sub(r'\n{3,}', '\n\n', s)

    # 11. Trim leading/trailing whitespace
    s = s.strip()

    return s

# ── Backup ───────────────────────────────────────────────
ts = time.strftime('%Y-%m-%d_%H%M%S')
backup_path = f'/tmp/zg-backup-pre-clean-{ts}.json'
shutil.copy2(ZG_DATA, backup_path)
print(f"Backup: {backup_path}")

# ── Load data ────────────────────────────────────────────
with open(ZG_DATA, 'r', encoding='utf-8') as f:
    data = json.load(f)

anki = json.loads(data[STORE_KEY])
deck = anki['decks'][TARGET_DECK_ID]
cards = deck['cards']

print(f"Deck: {deck['name']}, {len(cards)} cards")

# ── Clean all cards ──────────────────────────────────────
cleaned = 0
for card in cards:
    old_front = card['front']
    old_back = card['back']
    new_front = clean_html(old_front)
    new_back = clean_html(old_back)

    if new_front != old_front or new_back != old_back:
        card['front'] = new_front
        card['back'] = new_back
        cleaned += 1

print(f"Cleaned: {cleaned}/{len(cards)} cards")

# ── Write back ───────────────────────────────────────────
data[STORE_KEY] = json.dumps(anki, ensure_ascii=False)

tmp_path = '/tmp/zg-data-cleaned.json'
with open(tmp_path, 'w', encoding='utf-8') as f:
    json.dump(data, f, ensure_ascii=False)

print(f"Written to {tmp_path}")
print("Run: sudo mv /tmp/zg-data-cleaned.json /media/HDD_1TB/Zeitgeist/data/data.json")

# ── Show before/after samples ────────────────────────────
print("\n── Before/After samples ──")
# Find cards that had HTML
samples = []
for card in cards:
    if '<' in clean_html.__wrapped__ if hasattr(clean_html, '__wrapped__') else True:
        pass

# Just show a few that were actually cleaned
shown = 0
with open(backup_path, 'r') as f:
    old_data = json.load(f)
old_anki = json.loads(old_data[STORE_KEY])
old_cards = {c['id']: c for c in old_anki['decks'][TARGET_DECK_ID]['cards']}

for card in cards:
    old = old_cards.get(card['id'])
    if old and (old['front'] != card['front'] or old['back'] != card['back']):
        if shown >= 3:
            break
        print(f"\nCard: {card['id']}")
        print(f"  Front BEFORE: {old['front'][:120]}")
        print(f"  Front AFTER:  {card['front'][:120]}")
        print(f"  Back BEFORE:  {old['back'][:120]}")
        print(f"  Back AFTER:   {card['back'][:120]}")
        shown += 1


Explanation: The script has a bug in the sample section. Let me fix it and run it.

patch

# ── Show before/after samples ────────────────────────────
print("\n── Before/After samples ──")
# Find cards that had HTML
samples = []
for card in cards:
    if '<' in clean_html.__wrapped__ if hasattr(clean_html, '__wrapped__') else True:
        pass

# Just show a few that were actually cleaned
shown = 0
with open(backup_path, 'r') as f:
    old_data = json.load(f)
old_anki = json.loads(old_data[STORE_KEY])
old_cards = {c['id']: c for c in old_anki['decks'][TARGET_DECK_ID]['cards']}

for card in cards:
    old = old_cards.get(card['id'])
    if old and (old['front'] != card['front'] or old['back'] != card['back']):
        if shown >= 3:
            break
        print(f"\nCard: {card['id']}")
        print(f"  Front BEFORE: {old['front'][:120]}")
        print(f"  Front AFTER:  {card['front'][:120]}")
        print(f"  Back BEFORE:  {old['back'][:120]}")
        print(f"  Back AFTER:   {card['back'][:120]}")
        shown += 1

# ── Show before/after samples ────────────────────────────
print("\n── Before/After samples ──")
with open(backup_path, 'r') as f:
    old_data = json.load(f)
old_anki = json.loads(old_data[STORE_KEY])
old_cards = {c['id']: c for c in old_anki['decks'][TARGET_DECK_ID]['cards']}

shown = 0
for card in cards:
    old = old_cards.get(card['id'])
    if old and (old['front'] != card['front'] or old['back'] != card['back']):
        if shown >= 3:
            break
        print(f"\nCard: {card['id']}")
        print(f"  Front BEFORE: {old['front'][:120]}")
        print(f"  Front AFTER:  {card['front'][:120]}")
        print(f"  Back BEFORE:  {old['back'][:120]}")
        print(f"  Back AFTER:   {card['back'][:120]}")
        shown += 1


--- a//tmp/clean_anki_html.py
+++ b//tmp/clean_anki_html.py
@@ -113,19 +113,12 @@

 # ── Show before/after samples ────────────────────────────
 print("\n── Before/After samples ──")
-# Find cards that had HTML
-samples = []
-for card in cards:
-    if '<' in clean_html.__wrapped__ if hasattr(clean_html, '__wrapped__') else True:
-        pass
-
-# Just show a few that were actually cleaned
-shown = 0
 with open(backup_path, 'r') as f:
     old_data = json.load(f)
 old_anki = json.loads(old_data[STORE_KEY])
 old_cards = {c['id']: c for c in old_anki['decks'][TARGET_DECK_ID]['cards']}

+shown = 0
 for card in cards:
     old = old_cards.get(card['id'])
     if old and (old['front'] != card['front'] or old['back'] != card['back']):


Explanation: The script is fixed. Now let me runit.


terminal

Output:
Backup: /tmp/zg-backup-pre-clean-2026-06-11_114955.json
Deck: Zivilrecht, 91 cards
Cleaned: 67/91 cards
Written to /tmp/zg-data-cleaned.json
Run: sudo mv /tmp/zg-data-cleaned.json /media/HDD_1TB/Zeitgeist/data/data.json

── Before/After samples ──

Card: mq9b8jfo7bwu
  Front BEFORE: Pfandrechtserwerb
  Front AFTER:  Pfandrechtserwerb
  Back BEFORE:

Titel (Pfandbestellungsvertrag oder Gesetz)

Dingliche Berechtigung des Vormanns

Modus (”
  Back AFTER:   1. Titel (Pfandbestellungsvertrag oder Gesetz)
2. Dingliche Berechtigung des Vormanns
3. Modus (”Faustpfandprinzip”) → B

Card: mq9b8jfpbwb6
  Front BEFORE: Gutgläubiger Pfandrechtserwerb
  Front AFTER:  Gutgläubiger Pfandrechtserwerb
  Back BEFORE:  Bei fehlender dinglicher Berechtigung des Verpfänders möglich bei

Gültiger Pfandbestellungsvertrag

üb
  Back AFTER:   Bei fehlender dinglicher Berechtigung des Verpfänders möglich bei
• Gültiger Pfandbestellungsvertrag
• über eine bewegli

Card: mq9b8jfpj928
  Front BEFORE: Inwiefern ist die Akzessorietät bei der Hypothek gelockert
  Front AFTER:  Inwiefern ist die Akzessorietät bei der Hypothek gelockert
  Back BEFORE:  Hypothek bleibt formell bestehen, bis sie im Grundbuch gelöscht wird, auch wenn die Forderung getilgt ist -> Forderun
  Back AFTER:   Hypothek bleibt formell bestehen, bis sie im Grundbuch gelöscht wird, auch wenn die Forderung getilgt ist -> Forderungse


Sieht gut aus — 67/91 Karten bereinigt. Listen, Tags, Entities sind sauber. Jetzt noch rein damit:

Explanation: Sieht gut aus — 67 von 91 Karten wurden bereinigt. Die Before/After-Beispiele zeigen, dass `<ol>`, `<ul>`, `<li>`, `<br>` etc. sauber in Markdown-Listen umgewandelt wurden. Jetzt muss ich nur noch die Datei ins Ziel verschieben.

sudo mv /tmp/zg-data-cleaned.json /media/HDD_1TB/Zeitgeist/data/data.json && echo "OK" && /DATA/AppData/hermes/venv/bin/python -c "
import json
with open('/media/HDD_1TB/Zeitgeist/data/data.json') as f:
    data = json.load(f)
anki = json.loads(data['zeitgeist-anki-v2'])
deck = anki['decks']['mq0izjiyplw9']
# Check for any remaining HTML
html = sum(1 for c in deck['cards'] if '<' in c['front'] or '<' in c['back'])
print(f'Cards with remaining HTML: {html}/{len(deck[\"cards\"])}')
# Show a couple cleaned cards
for c in deck['cards'][:2]:
    print(f'\nF: {c[\"front\"][:100]}')
    print(f'B: {c[\"back\"][:100]}')
"


Output:
OK
Cards with remaining HTML: 2/91

F: Umfang des Pfandrechts gem § 451 ABGB
B: Dispositive Bestimmung: Im Zweifel gilt jedes Zubehör der Hauptsache als mitverpfändet.
• Zubehör mu

F: Welche Formvorschrift gilt für die Bürgschaft?
B: Schriftformgebot


Noch 2 Karten mit HTML-Resten — die hol ich mir kurz.

Explanation: 2 cards have remaining HTML. Let me check what they are and clean them.

/DATA/AppData/hermes/venv/bin/python << 'PYEOF'
import json

with open('/media/HDD_1TB/Zeitgeist/data/data.json') as f:
    data = json.load(f)
anki = json.loads(data['zeitgeist-anki-v2'])
deck = anki['decks']['mq0izjiyplw9']

for c in deck['cards']:
    if '<' in c['front'] or '<' in c['back']:
        print(f"ID: {c['id']}")
        print(f"Front: {c['front']}")
        print(f"Back: {c['back']}")
        print()
PYEOF


Output:
ID: mq9b8jfpn7zp
Front: Warnpflicht des Werkunternehmers
Back: § 1168a- Spährentheorie- Bei Unterlassung hat Werkbesteller Gewährleistungsansprüche

ID: mq9b8jfpf6ek
Front: Gesetzgebung des EU-Parlaments
Back: • Gemeinsam mit Rat
• Kein eigenes Initiativrecht
• Wahl KOM-Präs auch Vorschlag des EuR
• Zustimmung in bestimmten Fällen

zB Sanktionen bei Verletzung der Grundsätze der EU
• zB Beitritt neuer Mitgliedstaaten


Explanation:
Die beiden verbleibenden Karten haben:

1. `mq9b8jfpn7zp` - HTML-Kommentare `<!-- ... -->`
2. `mq9b8jfpf6ek` - Unvollständige `<li>` Tags (`<li>` und `</li>` ohne umschließende `<ul>`/`<ol>`)

Mein Cleaner hat HTML-Kommentare und isolierte `<li>` Tags nicht behandelt. Ich muss den Cleaner erweitern um:
- `<!-- ... -->` Kommentare zu entfernen
- Isolierte `<li>...</li>` ohne `<ul>`/`<ol>` zu verarbeiten

Ich kann das entweder per direktem Patch der beiden Karten fixen, oder den Cleaner updaten und nochmal laufen lassen. Direkter Patch ist schneller.

Für Karte 1: Einfach `<!-- notionvc: ... -->` entfernen
Für Karte 2: `<li>` und `</li>` entfernen (das `\n<li>\n` und `</li>`), den Inhalt als `•` belassen.

Ich mach das direkt per Script.

/DATA/AppData/hermes/venv/bin/python << 'PYEOF'
import json, re, shutil, time

ZG_DATA = '/media/HDD_1TB/Zeitgeist/data/data.json'

# Backup
ts = time.strftime('%Y-%m-%d_%H%M%S')
shutil.copy2(ZG_DATA, f'/tmp/zg-backup-final-{ts}.json')

with open(ZG_DATA, 'r') as f:
    data = json.load(f)

anki = json.loads(data['zeitgeist-anki-v2'])
cards = anki['decks']['mq0izjiyplw9']['cards']

for c in cards:
    # Remove HTML comments
    c['front'] = re.sub(r'<!--.*?-->', '', c['front'])
    c['back'] = re.sub(r'<!--.*?-->', '', c['back'])

    # Remove bare <li> / </li> tags
    c['front'] = re.sub(r'</?li>', '', c['front'])
    c['back'] = re.sub(r'</?li>', '', c['back'])

    # Clean up whitespace
    c['front'] = c['front'].strip()
    c['back'] = c['back'].strip()

data['zeitgeist-anki-v2'] = json.dumps(anki, ensure_ascii=False)

tmp = '/tmp/zg-data-final.json'
with open(tmp, 'w') as f:
    json.dump(data, f, ensure_ascii=False)

# Verify
html_left = sum(1 for c in cards if '<' in c['front'] or '<' in c['back'])
print(f"HTML remaining: {html_left}")

# Show the two fixed cards
for c in cards:
    if c['id'] in ('mq9b8jfpn7zp', 'mq9b8jfpf6ek'):
        print(f"\n{c['id']}:")
        print(f"  Front: {c['front'][:120]}")
        print(f"  Back:  {c['back'][:120]}")

print(f"\nReady. Run: sudo mv {tmp} {ZG_DATA}")
PYEOF


Output:
HTML remaining: 0

mq9b8jfpn7zp:
  Front: Warnpflicht des Werkunternehmers
  Back:  § 1168a- Spährentheorie- Bei Unterlassung hat Werkbesteller Gewährleistungsansprüche

mq9b8jfpf6ek:
  Front: Gesetzgebung des EU-Parlaments
  Back:  • Gemeinsam mit Rat
• Kein eigenes Initiativrecht
• Wahl KOM-Präs auch Vorschlag des EuR
• Zustimmung in bestimmten Fäll

Ready. Run: sudo mv /tmp/zg-data-final.json /media/HDD_1TB/Zeitgeist/data/data.json


Explanation: The output shows 0 HTML remaining and the two problematic cards are fixed. Now I need to move the final cleaned file into place.

sudo mv /tmp/zg-data-final.json /media/HDD_1TB/Zeitgeist/data/data.json && echo "OK — 0 HTML tags remaining, 91 clean cards"


Output: OK — 0 HTML tags remaining, 91 clean cards


Sauber! Hier die Zusammenfassung:

**Zivilrecht-Deck:** 91 Karten (1 bestehende + 90 importiert)
**Bereinigt:** Alle 67 Karten mit HTML-Tags → jetzt reines Markdown
**HTML-Reste:** 0 — alles weg (`<span>`, `<div>`, `<br>`, ` `, `<!--comments-->`, `<li>`)
**Format:** **Fett**, *Kursiv*, Listen (• und 1.), Links, RIS-Links werden vom Anki-Renderer automatisch verlinkt

Backups liegen unter `/tmp/zg-backup-*` falls was schiefgeht. Geh auf `http://172.29.0.1:8765/anki.html` → Zivilrecht, dann siehst du's.

Explanation:
The user is asking for a general update / status after the import and cleanup. Let me summarize what was done.

Done: 90 cards imported into the "Zivilrecht" deck, then all HTML stripped (67 cards had HTML, plus 2 with leftover comments/li tags), now 91 cards total (1 pre-existing + 90 new), all clean markdown. 0 HTML remaining. Server confirmed running.