PageSourceSearch

https://cropflow.com.br/js/profile.js

js cropflow.com.br collected 2026-10-01 03:52:42 UTC 22,356 bytes, 264 lines download raw bytes

1        let weatherDataCache = null;
2        let weatherTab = 'daily';
3        let finalizePhotos = [];
4        let finalizeVisitId = null;
5        let currentProfile = null;
6
7        function isTrialActive() {
8            if (!currentProfile) return true;
9            return currentProfile.plan !== 'pro';
10        }
11
12        async function debugProfile() {
13            const uid = currentUser?.id || 'N/A';
14            const cached = localStorage.getItem('cropflow_profile_' + uid);
15            let cachedPlan = '—';
16            try { cachedPlan = JSON.parse(cached||'{}')?.plan || '—'; } catch(_) {}
17
18            let dbPlan = '—', dbProEnd = '—', dbError = '';
19            try {
20                const { data, error } = await supabaseClient
21                    .from('profiles').select('plan, pro_end, is_admin').eq('user_id', uid).maybeSingle();
22                if (data) { dbPlan = data.plan; dbProEnd = data.pro_end || 'sem data'; }
23                else if (error) { dbError = error.code + ': ' + error.message; }
24                else { dbError = 'perfil não encontrado no banco'; }
25            } catch(e) { dbError = e.message; }
26
27            const msg = [
28                'DIAGNÓSTICO DO PERFIL',
29                '────────────────────────',
30                'ID: ' + uid.substring(0,8) + '...',
31                'Email: ' + (currentUser?.email || '—'),
32                '',
33                'BANCO DE DADOS:',
34                '  Plano: ' + dbPlan,
35                '  PRO até: ' + dbProEnd,
36                (dbError ? 'Erro: ' + dbError : ''),
37                '',
38                'CACHE LOCAL:',
39                '  Plano em cache: ' + cachedPlan,
40                '',
41                'APP AGORA:',
42                '  currentProfile.plan: ' + (currentProfile?.plan || 'null'),
43                '  isPro(): ' + isPro(),
44                '  isProExpired(): ' + isProExpired(),
45            ].filter(x => x !== undefined).join('\n');
46
47            alert(msg);
48        }
49
50        const APP_BUILD_TS = window.CF_BUILD || '';
51        const VAPID_PUBLIC_KEY = 'BCfHLUk_UNlSjCoIWIAHIEzebk4IznQxHfAolt1JuISe6iBRIYzbmlkH4QnQMvYHIW1q0PHGzZduxQXga6cXwUc';
52
53        // ── SVG Icon Helper ──────────────────────────────────────────
54        const _ICONS = {
55            check:         `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 13l4 4L19 7"/></svg>`,
56            checkCircle:   `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>`,
57            clock:         `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>`,
58            calendar:      `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>`,
59            alertTriangle: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`,
60            bell:          `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg>`,
61            edit:          `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>`,
62            trash:         `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4a1 1 0 011-1h4a1 1 0 011 1v2"/></svg>`,
63            send:          `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
63<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>`,
64            fileText:      `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>`,
65            lock:          `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg>`,
66            image:         `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>`,
67            target:        `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>`,
68            tool:          `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z"/></svg>`,
69            leaf:          `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 8C8 10 5.9 16.17 3.82 19.28A1 1 0 005 21 14.46 14.46 0 0022 7c0-1.66-.2-3.4-.59-5C21 2 18 1 17 1l-1 2.06A5 5 0 0117 8z"/><path d="M6.2 11.4A12 12 0 003 19"/></svg>`,
70            notepad:       `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8"/></svg>`,
71            arrowRight:    `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>`,
72            share:         `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>`,
73            star:          `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>`,
74            starEmpty:     `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>`,
75            mapPin:        `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></svg>`,
76            user:          `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`,
77            info:          `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>`,
78            trendingUp:    `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/></svg>`,
79
80            // ── Ampliação para substituir os emojis da interface ──────────────
81            // Emoji renderiza diferente em cada sistema, não aceita a cor da marca e
82            // é o marcador visual nº1 de produto improvisado. Mesma grade 24x24 e
83            // mesma espessura dos que já existiam, para o conjunto ler como um só.
84            x:             `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg>`,
85            plus:          `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>`,
86            search:        `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>`,
87            refresh:       `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-2.6-6.4"/><polyline points="21 3 21 9 15 9"/></svg>`,
88            settings:      `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.6 1.6 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.6 1.6 0 0 0-2.7 1.1V21a2 2 0 1 1-4 0v-.1A1.6 1.6 0 0 0 7.9 19.4a1.6 1.6 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.6 1.6 0 0 0-1.1-2.7H2a2 2 0 1 1 0-4h.1a1.6 1.6 0 0 0 1.1-2.7l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.6 1.6 0 0 0 1.8.3 1.6 1.6 0 0 0 1-1.5V2a2 2 0 1 1 4 0v.1a1.6 1.6 0 0 0 2.7 1.1l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.6 1.6 0 0 0-.3 1.8 1.6 1.6 0 0 0 1.5 1H22a2 2 0 1 1 0 4h-.1a1.6 1.6 0 0 0-1.5 1Z"/></svg>`,
89            shield:        `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z"/></svg>`,
90            clipboard:     `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3h6v3H9z"/><path d="M15 4.5h2.5A1.5 1.5 0 0 1 19 6v13.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 5 19.5V6a1.5 1.5 0 0 1 1.5-1.5H9"/><path d="M8.5 11h7M8.5 15h5"/></svg>`,
91            chart:         `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21h18"/><rect x="5" y="11" width="3.4" height="7" rx="1"/><rect x="10.3" y="6" width="3.4" height="12" rx="1"/><rect x="15.6" y="14" width="3.4" height="4" rx="1"/></svg>`,
92            sprout:        `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21V9"/><path d="M12 13c-3.3 0-5-1.9-5-5 3.3 0 5 1.9 5 5Z"/><path d="M12 13c3.3 0 5-1.9 5-5-3.3 0-5 1.9-5 5Z"/></svg>`,
93            bot:           `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 4c0 7.5-4.2 12-9.5 12H7c0-6.6 4.7-11.2 13-12Z"/><path d="M4.5 20c.7-3.6 2-6.3 4-8.2"/></svg>`,
94            link:          `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13.5a4 4 0 0 0 5.7.4l3-3a4 4 0 0 0-5.7-5.7l-1.7 1.7"/><path d="M14 10.5a4 4 0 0 0-5.7-.4l-3 3a4 4 0 0 0 5.7 5.7l1.7-1.7"/></svg>`,
95            camera:        `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 8h3l1.5-2h7L17 8h3a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1Z"/><circle cx="12" cy="13.5" r="3.2"/>
95</svg>`,
96            home:          `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 10.5 9-7 9 7"/><path d="M5.5 9.5V20h13V9.5"/></svg>`,
97            money:         `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2.5" y="6" width="19" height="12" rx="2"/><circle cx="12" cy="12" r="2.6"/><path d="M6 12h.01M18 12h.01"/></svg>`,
98            download:      `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12"/><polyline points="8 11 12 15 16 11"/><path d="M4 19h16"/></svg>`,
99            phone:         `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="2.5" width="12" height="19" rx="2.5"/><path d="M11 18.5h2"/></svg>`,
100            wifiOff:       `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3l20 18"/><path d="M5 12.5a11 11 0 0 1 3.5-2.3"/><path d="M8.5 16a6 6 0 0 1 2.2-1.4"/><path d="M12 20h.01"/><path d="M19.5 12.5a11 11 0 0 0-6-3.2"/></svg>`,
101            moon:          `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 14.5A8.5 8.5 0 0 1 9.5 4a8.5 8.5 0 1 0 10.5 10.5Z"/></svg>`,
102            cloud:         `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 18a4 4 0 0 1 .6-8A5.5 5.5 0 0 1 18 10.5a3.75 3.75 0 0 1-.4 7.5Z"/></svg>`,
103            book:          `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4.5A1.5 1.5 0 0 1 5.5 3H19v16H5.5A1.5 1.5 0 0 0 4 20.5Z"/><path d="M4 20.5A1.5 1.5 0 0 1 5.5 19H19v2H5.5A1.5 1.5 0 0 1 4 20.5Z"/></svg>`,
104        };
105        function _icon(name, size) {
106            const s = size || 16;
107            const svg = _ICONS[name] || _ICONS.info;
108            return svg.replace('<svg ', `<svg width="${s}" height="${s}" style="display:inline-block;vertical-align:middle;flex-shrink:0;" `);
109        }
110        // ────────────────────────────────────────────────────────────
111
112        const PROFILE_CACHE_VERSION = window.CF_BUILD || '';
113        function loadCachedProfile() {
114            if (!currentUser) return;
115
116            const savedVersion = localStorage.getItem('cropflow_cache_version');
117            if (savedVersion !== PROFILE_CACHE_VERSION) {
118
119                Object.keys(localStorage)
120                    .filter(k => k.startsWith('cropflow_profile_'))
121                    .forEach(k => localStorage.removeItem(k));
122                sessionStorage.removeItem('cropflow_promo_shown');
123                localStorage.removeItem('cropflow_last_promo');
124                localStorage.removeItem('cropflow_banner_dismissed');
125                localStorage.setItem('cropflow_cache_version', PROFILE_CACHE_VERSION);
126
127                return;
128            }
129
130            const cached = localStorage.getItem('cropflow_profile_' + currentUser.id);
131            if (cached) {
132                try { currentProfile = JSON.parse(cached); } catch(_) {}
133            }
134        }
135
136        function showUpgradeModal(message) {
137            if (message) {
138                const sub = document.getElementById('trialBannerSub');
139                if (sub) sub.textContent = message;
140            }
141            openModal('modalUpgrade');
142        }
143
144        // ── Planos ─────────────────────────────────────────────────────────
145        // O checkout anual precisa de um produto próprio na Kirvano. Enquanto o
146        // link não existir, a aba anual leva ao mensal — melhor do que abrir um
147        // link quebrado na hora de cobrar.
148        const CF_PLANOS = {
149            mensal: {
150                checkout: 'https://pay.kirvano.com/db528772-cd14-4119-bf5b-bcd6b820d244',
151                rotulo:   'Assinar PRO — R$ 29,99/mês'
152            },
153            anual: {
154                checkout: 'https://pay.kirvano.com/4ac3f4c2-cf52-4dc9-b908-74b1ac5fac7e',
155                rotulo:   'Assinar Plano Safra — R$ 249,00/ano'
156            }
157        };
158        let _cfPlanoSelecionado = 'mensal';
159
160        function cfSelecionarPlano(plano) {
161            if (!CF_PLANOS[plano]) return;
162            _cfPlanoSelecionado = plano;
163
164            const mensal = plano === 'mensal';
165            const elM = document.getElementById('cfPriceMensal');
166            const elA = document.getElementById('cfPriceAnual');
167            if (elM) elM.style.display = mensal ? 'block' : 'none';
168            if (elA) elA.style.display = mensal ? 'none'  : 'block';
169
170            document.getElementById('cfTabMensal')?.classList.toggle('is-active', mensal);
171            document.getElementById('cfTabAnual')?.classList.toggle('is-active', !mensal);
172
173            const btn = document.getElementById('cfBtnAssinar');
174            if (btn) btn.textContent = CF_PLANOS[plano].rotulo;
175        }
176
177        function assinarProKirvano() {
178            const plano = CF_PLANOS[_cfPlanoSelecionado] || CF_PLANOS.mensal;
179            const checkoutUrl = plano.checkout || CF_PLANOS.mensal.checkout;
180
181            if (_cfPlanoSelecionado === 'anual' && !plano.checkout) {
182                console.warn('[Planos] Checkout anual não configurado — caindo no mensal.');
183            }
184
185            const email = currentUser?.email || '';
186            // O e-mail precisa bater com o do webhook: é por ele que o pagamento
187            // encontra a conta e libera o PRO.
188            const url = email ? `${checkoutUrl}?email=${encodeURIComponent(email)}` : checkoutUrl;
189            if (typeof cfTrack === 'function') cfTrack('checkout_aberto', { plano: _cfPlanoSelecionado });
190            window.open(url, '_blank');
191        }
192
193        function showAlert(msg) {
194            return new Promise(resolve => {
195                const modal = document.getElementById('modalAlert');
196                document.getElementById('modalAlertText').textContent = msg;
197                modal.style.display = 'flex';
198                const btn = document.getElementById('modalAlert').querySelector('button');
199                const handler = () => { modal.style.display = 'none'; btn.removeEventListener('click', handler); resolve(); };
200                btn.addEventListener('click', handler);
201            });
202        }
203
204        function showConfirm(msg, confirmLabel = 'Confirmar', danger = true) {
205            return new Promise(resolve => {
206                const modal = document.getElementById('modalConfirm');
207                document.getElementById('modalConfirmText').textContent = msg;
208                const okBtn = document.getElementById('modalConfirmOk');
209                okBtn.textContent = confirmLabel;
210                okBtn.className = `flex-1 py-3 ${danger ? 'bg-red-600 hover:bg-red-500' : 'bg-green-600 hover:bg-green-500'} text-white rounded-xl font-semibold text-sm transition`;
211                modal.style.display = 'flex';
212                const yes = () => { cleanup(); resolve(true); };
213                const no  = () => { cleanup(); resolve(false); };
214                const cleanup = () => {
215                    modal.style.display = 'none';
216                    document.getElementById('modalConfirmOk').removeEventListener('click', yes);
217                    document.getElementById('modalConfirmCancel').removeEventListener('click', no);
218                };
219                document.getElementById('modalConfirmOk').addEventListener('click', yes);
220                document.getElementById('modalConfirmCancel').addEventListener('click', no);
221            });
222        }
223
224        function sanitize(str) {
225            if (str === null || str === undefined) return '';
226            return String(str)
227                .replace(/&/g, '&amp;')
228                .replace(/</g, '&lt;')
229                .replace(/>/g, '&gt;')
230                .replace(/"/g, '&quot;')
231                .replace(/'/g, '&#x27;');
232        }
233
234        // As chaves de onboarding eram globais no navegador. Uma segunda conta na
235        // mesma maquina nascia com o guia ja marcado como concluido e nunca via
236        // o passo a passo. Agora cada conta tem o seu.
237        function _ob(chave) {
238            const id = (typeof currentUser !== 'undefined' && currentUser && currentUser.id) ? currentUser.id : 'anon';
239            return chave + '__' + id;
240        }
241
242        function escAttrJs(str) {
243            if (str === null || str === undefined) return '';
244            return String(str)
245                .replace(/\\/g, '\\\\')
246                .replace(/'/g, "\\'")
247                .replace(/\r?\n/g, '\\n')
248                .replace(/&/g, '&amp;')
249                .replace(/"/g, '&quot;')
250                .replace(/</g, '&lt;')
251                .replace(/>/g, '&gt;');
252        }
253
254        function sanitizeAI(str) {
255            if (!str) return '';
256            return String(str)
257                .replace(/&/g, '&amp;')
258                .replace(/</g, '&lt;')
259                .replace(/>/g, '&gt;')
260                .replace(/"/g, '&quot;')
261                .replace(/\n/g, '<br>')
262                .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
263                .replace(/\*(.+?)\*/g, '<em>$1</em>');
264        }

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.