⚖️

Complaints AI

Enter a problem — broken router, wrong delivery, fine — and get a professional complaint with legal citations, ready to send.

📝10+ complaint templates
🤖AI-generated text with legal citations
⚖️Automatic Czech law paragraphs
📤Export as printable document or email
💾Save drafts and edit later

⚖️ Complaints AI

Your complaints

0
Total
0
Sent
0
Drafts
⚖️

No complaints yet. Create your first one!

⚖️ Complaint Templates

Select a template to start

📝 Create Complaint

Fill in the details

⚖️ Your Complaint

Review and export

💾 Drafts

Your saved complaints

⚙️ Settings

General

📊
0
Drafts
0
Generated
FREE
Plan
🌍
Language
English
Quick Actions
📤
Export Data
Download as JSON
🗑️
Clear All Data
Remove drafts & history
💬
Give Feedback
Bug, idea or question
👑
Pro
33,07 € · Lifetime
Unlimited complaints Advanced AI generation PDF export + formatting
⚖️
Complaints AI
v1.0 · appvantagelabs2@gmail.com
👑

Complaints AI Pro

Unlock all features. No subscriptions, no limits.

🔓 Unlimited complaints
🤖 Advanced AI text generation
📄 PDF export + formatting
⚖️ Extended legal database (100+ citations)
📊 Success rate analytics
799 Kč
One-time payment · Lifetime Pro
👑 Unlock Pro
⚖️ Your e-mail address is stored only to deliver and activate your Pro license — never used for marketing. Deletion on request: appvantagelabs2@gmail.com
After paying, enter the e-mail you used at checkout — Pro activates automatically.
💡 Already paid — contact https://casuyi.com/#legal for unlock
`); win.document.close(); setTimeout(() => { win.print(); }, 500); } function shareByEmail() { const text = document.getElementById('complaint-output').textContent; const subject = encodeURIComponent(t('emailSubject')); const body = encodeURIComponent(text); window.open(`mailto:?subject=${subject}&body=${body}`); } // ═══════════════════════════════════════ // UTILS // ═══════════════════════════════════════ function today() { return new Date().toISOString().split('T')[0]; } function escHtml(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } async function refreshDashboard() { try { const d = await dbGetAll('drafts'); renderDashboard(d); updateSettingsStats(); updateComplaintsPremiumUI(); } catch (e) { /* db may not be open yet */ } } function showToast(msg) { const t = document.getElementById('toast'); t.textContent = msg; t.classList.add('show'); setTimeout(() => t.classList.remove('show'), 3000); } // ═══════════════════════════════════════ // PREMIUM / STRIPE // ═══════════════════════════════════════ const FREE_LIMIT_COMPLAINTS = 5; function isComplaintsPremium() { return localStorage.getItem('complaintsai_premium') === 'true'; } function setComplaintsPremium(unlock) { localStorage.setItem('complaintsai_premium', unlock ? 'true' : 'false'); updateComplaintsPremiumUI(); } async function updateComplaintsPremiumUI() { const premium = isComplaintsPremium(); [document.getElementById('pro-badge'), document.getElementById('pro-badge-templates')].forEach(badge => { if (badge) badge.style.display = premium ? 'inline-block' : 'none'; }); const limited = document.getElementById('limited-banner'); const count = await getCurrentComplaintCount(); if (limited) limited.style.display = (!premium && count >= FREE_LIMIT_COMPLAINTS) ? 'flex' : 'none'; } function getCurrentComplaintCount() { return new Promise((resolve) => { dbGetAll('drafts').then(data => resolve(data.length)); }); } async function updateSettingsStats() { const drafts = await dbGetAll('drafts'); const total = drafts.length; const draftsOnly = drafts.filter(d => !d.complaint && d.templateId).length; const generated = total - draftsOnly; const premium = isComplaintsPremium(); const draftsEl = document.getElementById('stat-drafts-count'); const complaintsEl = document.getElementById('stat-complaints-count'); const proEl = document.getElementById('stat-pro-status'); if (draftsEl) draftsEl.textContent = draftsOnly; if (complaintsEl) complaintsEl.textContent = generated > 0 ? generated : total; if (proEl) { proEl.textContent = premium ? 'PRO' : 'FREE'; proEl.style.color = premium ? 'var(--success)' : 'var(--text-tertiary)'; } } async function clearAllData() { if (!confirm(t('clearAllConfirm'))) return; const tx = db.transaction('drafts', 'readwrite'); tx.objectStore('drafts').clear(); tx.oncomplete = () => { showToast(t('deletedAll')); updateSettingsStats(); renderDashboard([]); }; tx.onerror = () => showToast(t('errorClear')); } async function exportData() { const drafts = await dbGetAll('drafts'); if (drafts.length === 0) { showToast(t('noExport')); return; } const blob = new Blob([JSON.stringify(drafts, null, 2)], {type: 'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `complaints-ai-export-${today()}.json`; a.click(); URL.revokeObjectURL(url); showToast(t('exportDone')); } // ═══════════════════════════════════════ // I18N // ═══════════════════════════════════════ const translations = { cs: { title_main: '⚖️ Generátor Stížností', title_templates: '⚖️ Šablony stížností', desc_templates: 'Vyberte šablonu a začněte', empty_state: 'Zatím žádné stížnosti. Vytvořte první!', empty_start: '▶ Začít ze šablony', empty_featured: 'Nejčastější stížnosti', sent_of: '{s}/{t} odeslano', paper_note: 'Dokument je hotov k odeslání — e-mailem nebo doporučenou poštou.', legal_ref: 'Právní rámec', onboarding_desc: 'Popište problém — rozbitý router, špatná dodávka, pokuta — a dostanete profesionální stížnost s citacemi zákonů, připravenou k odeslání.', nav_templates: 'Šablony', nav_drafts: 'Návrhy', title_create: '📝 Vytvořit stížnost', desc_create: 'Vyplňte údaje', label_name: 'Vaše jméno', label_address: 'Vaše adresa', label_recipient: 'Přijímající firma', label_problem: 'Co se stalo? (popište problém)', label_order: 'Číslo objednávky/smlouvy (volitelné)', label_resolution: 'Očekávané řešení', opt_repair: '🔧 Opravit/vyřešit problém', opt_refund: '💰 Vratit peníze', opt_exchange: '🔄 Vyměnit za funkční', opt_compensation: '💵 Kompenzace', opt_cancel: '❌ Zrušit smlouvu bez poplatku', opt_investigate: '🔍 Prošetřit problém', btn_start: 'Začít vytvářet', btn_generate: '⚡ Vytvořit stížnost', btn_save_draft: '💾 Uložit návrh', title_output: '⚖️ Vaše stížnost', desc_output: 'Zkontrolujte a exportujte', btn_copy: '📋 Kopírovat', btn_print: '🖨️ Tisknout', btn_email: '📧 E-mail', btn_save_as_draft: '💾 Uložit jako návrh', btn_new_complaint: '← Nová stížnost', title_drafts: '💾 Návrhy', desc_drafts: 'Vámi uložené stížnosti', settings_general: 'Obecné', settings_language: 'Jazyk', settings_help: 'Nápověda a podpora', settings_feedback: 'Zpětná vazba', settings_feedback_sub: 'Bug, nápad nebo dotaz — čteme vše', settings_pro: 'Complaints AI Pro', settings_pro_sub: 'Odemknout všechny funkce · jednorázově 33,07 €', settings_clear_data: 'Smazat všechna data', settings_clear_data_sub: 'Odstranit všechny návrhy a historii', settings_export: 'Exportovat data', settings_export_sub: 'Stáhnout vaše stížnosti jako JSON', nav_settings: 'Nastavení', btn_back: 'Zpět', dashTotal: 'Celkem', dashSent: 'Odesláno', dashDrafts: 'Návrhy', dashNoDetails: 'Bez detailů', dashSaved: 'Uložená stížnost', emptyDrafts: 'Zatím žádné návrhy. Vytvořte a uložte stížnost.', viewDraft: '📄 Zobrazit', deleteDraft: '🗑️', tplSuccess: 'úspěšnost', statusDraft: 'návrh', statusSent: 'odesláno', noProblem: 'Popište, prosím, problém', freeLimit: 'Limit zdarma: 5 stížností. Vylepšete na Pro!', saveDraftToast: '💾 Návrh uložen!', saveError: 'Chyba při ukládání návrhu', saveGenToast: '💾 Uloženo jako návrh!', deleteConfirm: 'Smazat tento návrh?', deleted: '🗑️ Návrh smazán', deletedAll: '🗑️ Všechna data smazána', clearAllConfirm: 'Smazat všechny návrhy a historii? Tuto akci nelze vrátit.', errorClear: 'Chyba při mazání dat', noExport: 'Žádná data k exportu', exportDone: '📤 Data exportována!', emailSubject: 'Complaints AI — Stížnost', tpl_return: 'Vrácení produktu (Reklamace)', tpl_cancel_contract: 'Výpověď smlouvy', tpl_late_delivery: 'Pozdní dodání', tpl_fine: 'Námitka proti pokutě', tpl_service_complaint: 'Stížnost na službu', tpl_wifi_router: 'Problém s WiFi/routerem', tpl_noise: 'Stížnost na hluk', tpl_insurance: 'Pojistné zamítnutí', tpl_salary: 'Pracovní spor', tpl_data_protection: 'Ochrana dat' }, sk: { title_main: '⚖️ Generátor Stížností', title_templates: '⚖️ Šablony stížností', desc_templates: 'Vyberte šablónu a začnite', empty_state: 'Zatiaľ žiadne sťažnosti. Vytvorte prvú!', empty_start: '▶ Začať zo šablóny', empty_featured: 'Najčastejšie sťažnosti', sent_of: '{s}/{t} odoslaných', paper_note: 'Dokument je pripravený na odoslanie — e-mailom alebo doporučenou poštou.', legal_ref: 'Právna podstata', onboarding_desc: 'Popíšte problém — rozbitý router, zlá dodávka, pokuta — a dostanete profesionálnu stížnosť s citáciami zákonov, pripravenú na odoslanie.', nav_templates: 'Šablóny', nav_drafts: 'Návrhy', title_create: '📝 Vytvoriť stížnosť', desc_create: 'Vyplňte údaje', label_name: 'Vaše meno', label_address: 'Vaša adresa', label_recipient: 'Prijímajúca firma', label_problem: 'Čo sa stalo? (opíšte problém)', label_order: 'Číslo objednávky/zmluvy (voliteľné)', label_resolution: 'Očakávané riešenie', opt_repair: '🔧 Opraviť/vyriešiť problém', opt_refund: '💰 Vrátiť peniaze', opt_exchange: '🔄 Vymeniť za funkčný', opt_compensation: '💵 Kompenzácia', opt_cancel: '❌ Zrušiť zmluvu bez poplatku', opt_investigate: '🔍 Preskúmať problém', btn_start: 'Začať vytvárať', btn_generate: '⚡ Vytvoriť stížnosť', btn_save_draft: '💾 Uložiť návrh', title_output: '⚖️ Vaša stížnosť', desc_output: 'Skontrolujte a exportujte', btn_copy: '📋 Kopírovať', btn_print: '🖨️ Tlačiť', btn_email: '📧 E-mail', btn_save_as_draft: '💾 Uložiť ako návrh', btn_new_complaint: '← Nová stížnosť', title_drafts: '💾 Návrhy', desc_drafts: 'Vami uložené stížnosti', settings_general: 'Všeobecné', settings_language: 'Jazyk', settings_help: 'Pomoc a podpora', settings_feedback: 'Spätná väzba', settings_feedback_sub: 'Bug, nápad alebo otázka — čítame všetko', settings_pro: 'Complaints AI Pro', settings_pro_sub: 'Odomknúť všetky funkcie · jednorazovo 33,07 €', settings_clear_data: 'Vymazať všetky dáta', settings_clear_data_sub: 'Odstrániť všetky návrhy a históriu', settings_export: 'Exportovať dáta', settings_export_sub: 'Stiahnuť vaše sťažnosti ako JSON', nav_settings: 'Nastavenia', btn_back: 'Späť', dashTotal: 'Celkom', dashSent: 'Odoslané', dashDrafts: 'Návrhy', dashNoDetails: 'Bez detailov', dashSaved: 'Uložená stížnosť', emptyDrafts: 'Zatiaľ žiadne návrhy. Vytvorte a uložte stížnosť.', viewDraft: '📄 Zobraziť', deleteDraft: '🗑️', tplSuccess: 'úspešnosť', statusDraft: 'návrh', statusSent: 'odoslané', noProblem: 'Prosím, opíšte problém', freeLimit: 'Limit zadarmo: 5 stížností. Ubezpečte si Pro!', saveDraftToast: '💾 Návrh uložený!', saveError: 'Chyba pri ukladaní návrhu', saveGenToast: '💾 Uložené ako návrh!', deleteConfirm: 'Vymazať tento návrh?', deleted: '🗑️ Návrh vymazaný', deletedAll: '🗑️ Všetky dáta vymazané', clearAllConfirm: 'Vymazať všetky návrhy a históriu? Tuto akciu nemožno vrátiť.', errorClear: 'Chyba pri mazaní dát', noExport: 'Žiadne dáta na export', exportDone: '📤 Dáta exportované!', emailSubject: 'Complaints AI — Stížnosť', tpl_return: 'Vratok tovaru (Reklamácia)', tpl_cancel_contract: 'Výpoveď zmluvy', tpl_late_delivery: 'Meškanie pri dodaní', tpl_fine: 'Námietka voči pokute', tpl_service_complaint: 'Stížnosť na službu', tpl_wifi_router: 'Problém s WiFi/rúterom', tpl_noise: 'Stížnosť na hluk', tpl_insurance: 'Zamietnutie poistnej udalosti', tpl_salary: 'Pracovný spor', tpl_data_protection: 'Ochrana údajov' }, en: { title_main: '⚖️ Complaints AI', empty_state: 'No complaints yet. Create your first one!', empty_start: '▶ Start from a template', empty_featured: 'Most common complaints', title_templates: '⚖️ Complaint Templates', desc_templates: 'Select a template to start', sent_of: '{s}/{t} sent', paper_note: 'Document ready to send — by email or registered post.', legal_ref: 'Legal basis', onboarding_desc: 'Enter a problem — broken router, wrong delivery, fine — and get a professional complaint with legal citations, ready to send.', nav_templates: 'Templates', nav_drafts: 'Drafts', title_create: '📝 Create Complaint', desc_create: 'Fill in the details', label_name: 'Your Name', label_address: 'Your Address', label_recipient: 'Recipient Company', label_problem: 'What happened? (describe the problem)', label_order: 'Order/Contract Number (optional)', label_resolution: 'Expected Resolution', opt_repair: '🔧 Repair/fix the issue', opt_refund: '💰 Full refund', opt_exchange: '🔄 Replace with working product', opt_compensation: '💵 Compensation', opt_cancel: '❌ Cancel contract without penalty', opt_investigate: '🔍 Investigate the issue', btn_start: 'Start Creating', btn_generate: '⚡ Generate Complaint', btn_save_draft: '💾 Save Draft', title_output: '⚖️ Your Complaint', desc_output: 'Review and export', btn_copy: '📋 Copy', btn_print: '🖨️ Print', btn_email: '📧 Email', btn_save_as_draft: '💾 Save as Draft', btn_new_complaint: '← New Complaint', title_drafts: '💾 Drafts', desc_drafts: 'Your saved complaints', settings_general: 'General', settings_language: 'Language', settings_help: 'Help & Support', settings_feedback: 'Give Feedback', settings_feedback_sub: 'Bug, idea or question — we read everything', settings_pro: 'Complaints AI Pro', settings_pro_sub: 'Unlock all features · one-time 33,07 €', settings_clear_data: 'Clear All Data', settings_clear_data_sub: 'Remove all drafts and history', settings_export: 'Export Data', settings_export_sub: 'Download your complaints as JSON', nav_settings: 'Settings', btn_back: 'Back', dashTotal: 'Total', dashSent: 'Sent', dashDrafts: 'Drafts', dashNoDetails: 'No details', dashSaved: 'Saved complaint', emptyDrafts: 'No drafts yet. Create and save a complaint.', viewDraft: '📄 View', deleteDraft: '🗑️', tplSuccess: 'success rate', statusDraft: 'draft', statusSent: 'sent', noProblem: 'Please describe the problem', freeLimit: 'Free limit: 5 complaints. Upgrade to Pro!', saveDraftToast: '💾 Draft saved!', saveError: 'Error saving draft', saveGenToast: '💾 Saved as draft!', deleteConfirm: 'Delete this draft?', deleted: '🗑️ Draft deleted', deletedAll: '🗑️ All data cleared', clearAllConfirm: 'Delete all drafts and history? This cannot be undone.', errorClear: 'Error clearing data', noExport: 'No data to export', exportDone: '📤 Data exported!', emailSubject: 'Complaints AI — Complaint', tpl_return: 'Product Return', tpl_cancel_contract: 'Cancel Contract', tpl_late_delivery: 'Late Delivery', tpl_fine: 'Dispute a Fine', tpl_service_complaint: 'Service Complaint', tpl_wifi_router: 'WiFi/Router Issues', tpl_noise: 'Noise Complaint', tpl_insurance: 'Insurance Claim Denial', tpl_salary: 'Salary/Work Dispute', tpl_data_protection: 'Data Protection' } }; // First start: detect from the system/browser locale. // cs → Czech, sk/sz (Slovak) → Slovak, en or anything else/unknown → English. // One-time guess: the result is saved on first start; explicit choice always wins after that. function detectLang() { const nav = (typeof navigator !== 'undefined') ? navigator : {}; const candidates = [nav.language, ...((nav.languages || [])).slice()]; for (const c of candidates) { if (!c) continue; const code = String(c).toLowerCase().split('-')[0]; if (code === 'cs') return 'cs'; if (code === 'sk' || code === 'sz') return 'sk'; } return 'en'; } let currentLang = localStorage.getItem('complaints_lang') || detectLang(); if (!localStorage.getItem('complaints_lang')) localStorage.setItem('complaints_lang', currentLang); function t(key) { return (translations[currentLang] && translations[currentLang][key]) || key; } // ── Per-language demo complaints ────────────────────────────────── // Each language gets its own realistic sample items, so switching language // re-seeds the demo set (only when the DB still holds the untouched demo set). const DEMO_COMPLAINTS = { cs: [ { type: 'return', problem: 'Porouchaný WiFi router od O2', resolution: 'refund', demo: true, date: new Date().toISOString() }, { type: 'late_delivery', problem: 'Balík nepřišel ve stanovené lhůtě', resolution: 'compensation', demo: true, date: new Date().toISOString() }, ], sk: [ { type: 'return', problem: 'Poškodený WiFi router od O2', resolution: 'refund', demo: true, date: new Date().toISOString() }, { type: 'late_delivery', problem: 'Balenie nedorazilo v stanovenom termíne', resolution: 'compensation', demo: true, date: new Date().toISOString() }, ], en: [ { type: 'return', problem: 'Broken WiFi router from the carrier', resolution: 'refund', demo: true, date: new Date().toISOString() }, { type: 'late_delivery', problem: 'Package did not arrive on time', resolution: 'compensation', demo: true, date: new Date().toISOString() }, ], }; async function seedDemoFor(lang) { const demo = DEMO_COMPLAINTS[lang] || DEMO_COMPLAINTS.en; for (const dc of demo) { await dbAdd('drafts', { ...dc, status: 'sent' }); } } // Replace demo set after a language switch — only when everything in the DB is // still the untouched demo set (all drafts flagged demo). Real drafts survive. async function regenerateDemoFor(newLang) { const drafts = await dbGetAll('drafts'); if (drafts.length === 0) return; if (!drafts.every(d => d.demo === true)) { renderDashboard(drafts); updateSettingsStats(); return; } for (const d of drafts) await dbDelete('drafts', d.id); await seedDemoFor(newLang); const fresh = await dbGetAll('drafts'); renderDashboard(fresh); updateSettingsStats(); } function setLang(lang) { currentLang = lang; localStorage.setItem('complaints_lang', lang); applyLang(); regenerateDemoFor(lang).catch(() => {}); } function toggleLangPicker() { // No-op: language switching is now done via flag buttons } function updateLangButtons() { document.querySelectorAll('.lang-btn').forEach(btn => btn.classList.remove('active')); const activeBtn = document.getElementById('lang-' + currentLang); if (activeBtn) activeBtn.classList.add('active'); } // ── Stripe Pro payment links (per-language currency) ──────────────────────── // cs: 33,07 € — the live test-mode Payment Link (see Stripe dashboard). // sk / en: create a Payment Link with the local-currency price in the Stripe // dashboard, then paste its buy.stripe.com URL into the sk: / en: entries // below — the button automatically follows the active language. Until those // links exist, all languages fall back to the CZK link (Stripe charges CZK). const STRIPE_PRO_LINKS = { cs: 'https://buy.stripe.com/8x25kDgBc7oa3wG5angYU01', sk: 'https://buy.stripe.com/8x25kDgBc7oa3wG5angYU01', // TODO: replace with the EUR (SK) Payment Link en: 'https://buy.stripe.com/8x25kDgBc7oa3wG5angYU01' // TODO: replace with the USD (US) Payment Link }; function stripeProLink() { return STRIPE_PRO_LINKS[currentLang] || STRIPE_PRO_LINKS.cs; } function applyLang() { document.querySelectorAll('[data-i18n]').forEach(el => el.textContent = t(el.getAttribute('data-i18n'))); document.querySelectorAll('[data-i18n-opt]').forEach(el => el.textContent = t(el.getAttribute('data-i18n-opt'))); const stripeBtn = document.getElementById('btn-stripe-pro'); if (stripeBtn) stripeBtn.href = stripeProLink(); const langNames = { cs: 'Čeština', sk: 'Slovenčina', en: 'English' }; const langCurrent = document.getElementById('complaints-ai-lang-current'); if (langCurrent) langCurrent.textContent = langNames[currentLang] || currentLang; updateLangButtons(); } // Initialize lang display on load (function initLangDisplay() { const langNames = { cs: 'Čeština', sk: 'Slovenčina', en: 'English' }; const langCurrent = document.getElementById('complaints-ai-lang-current'); if (langCurrent) langCurrent.textContent = langNames[currentLang] || currentLang; updateLangButtons(); })(); // ═══════════════════════════════════════ // INIT // ═══════════════════════════════════════ // ═══════════════════════════════════════ // DASHBOARD RENDER // ═══════════════════════════════════════ function renderDashboard(drafts) { const total = drafts.length; const sent = drafts.filter(d => d.status === 'sent').length; const draftsCount = drafts.filter(d => d.status === 'draft').length; document.getElementById('stat-total').textContent = total; document.getElementById('stat-sent').textContent = sent; document.getElementById('stat-drafts').textContent = draftsCount; // P2: Sent as mini-donut (sent / total) const donutEl = document.getElementById('sent-donut'); if (donutEl && total > 0) { donutEl.style.display = 'block'; const C = 2 * Math.PI * 22; const frac = Math.min(sent / total, 1); document.getElementById('sent-arc').style.strokeDasharray = (C * frac).toFixed(1) + ' ' + C.toFixed(1); document.getElementById('sent-donut-num').textContent = sent + '/' + total; document.getElementById('stat-sent-label').textContent = t('sent_of').replace('{s}', sent).replace('{t}', total); } else if (donutEl) donutEl.style.display = 'none'; if (total === 0) { document.getElementById('empty-state').style.display = 'block'; document.getElementById('complaints-list').style.display = 'none'; // Featured: top-3 templates on empty dashboard const feat = TEMPLATES.slice(0, 3); const fdiv = document.getElementById('empty-featured'); if (fdiv) { fdiv.innerHTML = '
' + t('empty_featured') + '
' + feat.map(x => '
' + x.icon + '' + tplName(x) + '' + x.successRate + '
').join(''); } } else { document.getElementById('empty-state').style.display = 'none'; document.getElementById('complaints-list').style.display = 'block'; let html = ''; drafts.forEach(d => { const tplId = d.type || d.templateId || 'return'; const icon = { return: '🔄', cancel_contract: '❌', late_delivery: '📦', fine: '💸', service_complaint: '🔧', wifi_router: '📡', noise: '🔇', insurance: '🛡️', salary: '💼', data_protection: '🔒' }[tplId] || '⚖️'; const _tpl = TEMPLATES.find(x => x.id === tplId); const tplNameText = _tpl ? tplName(_tpl) : (d.templateName || t('dashSaved')); const summary = d.problem ? d.problem.substring(0, 50) + (d.problem.length > 50 ? '...' : '') : (d.complaint ? 'Saved complaint' : 'No details'); const escSummary = escHtml(summary); const escTplName = escHtml(tplNameText); html += '
'; html += '
'; html += '' + icon + ''; html += '
' + escTplName + '
'; html += '
' + escSummary + '
'; html += '
'; html += '' + (d.status || 'draft') + ''; html += '
'; }); document.getElementById('complaints-list').innerHTML = html; } } async function init() { await openDB(); // SECURITY 2026-08-19: ?unlock=premium auto-unlock backdoor REMOVED. // Pro activation is verified server-side via api.casuyi.com entitlement (manual activation until live). updateComplaintsPremiumUI(); applyLang(); // Load demo data on first launch (per-language) let drafts = await dbGetAll('drafts'); if (drafts.length === 0) { await seedDemoFor(currentLang); drafts = await dbGetAll('drafts'); } renderDashboard(drafts); updateSettingsStats(); // Show onboarding first (it's the active screen by default) showScreen('screen-onboarding'); } init();