Files
oca-py.org/faq/index.html
T
webmanager 18638c68f5 FAQ: login con Basic Auth para el webhook
Como el webhook ahora pide Basic Auth, la página muestra un formulario de
acceso y manda Authorization: Basic en cada request (guarda en sessionStorage,
no embebe credenciales en el HTML). En 401 vuelve a pedir el login.
2026-11-07 17:37:04 +00:00

301 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet, noimageindex" />
<meta name="googlebot" content="noindex, nofollow" />
<title>FAQ — OCA Paraguay</title>
<link rel="icon" type="image/svg+xml" href="../img/favicon.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Sora:wght@600;700;800&display=swap" rel="stylesheet" />
<style>
:root{
--plum:#714B67; --plum-dark:#4b2f43; --violet:#6C4CE0; --violet-soft:#8f7bf0;
--mint:#16C79A; --mint-dark:#0e8f6e; --py-red:#D52B1E; --py-blue:#0038A8;
--ink:#231b2b; --muted:#6b6473; --line:#e7e3ec; --bg:#faf9fc;
}
*{box-sizing:border-box}
body{margin:0;font-family:Inter,system-ui,sans-serif;background:var(--bg);color:var(--ink);line-height:1.55}
header{background:#fff;border-bottom:1px solid var(--line);position:sticky;top:0;z-index:20}
.wrap{max-width:920px;margin:0 auto;padding:0 20px}
.head{display:flex;align-items:center;gap:14px;padding:14px 0;flex-wrap:wrap}
.head img{width:38px;height:38px;border-radius:9px}
.head h1{font-family:Sora,sans-serif;font-size:1.05rem;margin:0}
.head h1 small{display:block;font-family:Inter,sans-serif;font-weight:500;font-size:.72rem;color:var(--muted);letter-spacing:.04em;text-transform:uppercase}
.tools{margin-left:auto;display:flex;align-items:center;gap:10px}
.noidx{font-size:.64rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--py-red);border:1px solid currentColor;border-radius:999px;padding:3px 9px;white-space:nowrap}
.lang{display:inline-flex;background:#f2eff8;border-radius:999px;padding:3px}
.lang button{border:none;background:transparent;font:inherit;font-size:.76rem;font-weight:700;padding:5px 11px;border-radius:999px;cursor:pointer;color:var(--muted)}
.lang button.on{background:#fff;color:var(--violet);box-shadow:0 1px 3px rgba(0,0,0,.12)}
main{padding:26px 0 60px}
.bar{margin-bottom:22px}
.bar input{width:100%;padding:12px 15px;border:1px solid var(--line);border-radius:12px;font:inherit;background:#fff}
.bar input:focus{outline:2px solid rgba(108,76,224,.22);border-color:var(--violet-soft)}
.msg{padding:12px 16px;border-radius:12px;margin-bottom:18px;font-size:.9rem;display:none}
.msg.show{display:block}
.msg.err{background:#fdf1ef;color:#8f2117;border:1px solid #f0c9c4}
.auth{background:#fff;border:1px solid var(--line);border-radius:14px;padding:18px 20px;display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end}
.auth h2{font-family:Sora,sans-serif;font-size:.95rem;margin:0 0 12px;width:100%;color:var(--muted)}
.auth input{flex:1 1 160px;padding:11px 13px;border:1px solid var(--line);border-radius:10px;font:inherit;background:#fff}
.auth input:focus{outline:2px solid rgba(108,76,224,.22);border-color:var(--violet-soft)}
.auth button{font:inherit;font-weight:600;border-radius:10px;border:1px solid transparent;padding:11px 18px;cursor:pointer;background:var(--violet);color:#fff}
.auth .err{width:100%;margin:4px 0 0;font-size:.85rem;color:var(--py-red);font-weight:600;min-height:1em}
.area{margin:0 0 26px}
.area h2{font-family:Sora,sans-serif;font-size:.8rem;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);margin:0 0 10px}
.card{background:#fff;border:1px solid var(--line);border-radius:14px;margin-bottom:10px;overflow:hidden;transition:border-color .18s,box-shadow .18s}
.card[open]{border-color:var(--violet-soft);box-shadow:0 8px 26px rgba(108,76,224,.09)}
.card summary{cursor:pointer;padding:15px 18px;font-weight:600;list-style:none;display:flex;align-items:center;gap:12px}
.card summary::-webkit-details-marker{display:none}
.card summary::after{content:"+";margin-left:auto;color:var(--violet);font-weight:800;font-size:1.15rem;line-height:1}
.card[open] summary::after{content:"–"}
.card .ans{padding:0 18px 17px;margin:0;white-space:pre-line;color:#41394a;font-size:.93rem}
.empty{color:var(--muted);padding:16px 0}
footer{border-top:1px solid var(--line);background:#fff;padding:18px 0;font-size:.76rem;color:var(--muted);text-align:center}
</style>
</head>
<body>
<header>
<div class="wrap head">
<img src="../img/logo.svg" alt="OCA Paraguay" width="38" height="38" />
<h1><span data-i18n="title">Preguntas frecuentes</span><small data-i18n="subtitle">OCA Paraguay · uso interno</small></h1>
<div class="tools">
<span class="noidx" data-i18n="badge">no indexada</span>
<span class="lang" role="group" aria-label="Language">
<button type="button" data-lang="es">ES</button>
<button type="button" data-lang="en">EN</button>
</span>
</div>
</div>
</header>
<main class="wrap">
<div class="msg" id="msg"></div>
<form class="auth" id="auth" hidden>
<h2 data-i18n="loginTitle">Acceso</h2>
<input id="au" data-i18n-placeholder="user" placeholder="Usuario" autocomplete="username" autocapitalize="none" />
<input id="ap" type="password" data-i18n-placeholder="pass" placeholder="Contraseña" autocomplete="current-password" />
<button type="submit" data-i18n="enter">Entrar</button>
<p class="err" id="auth-err"></p>
</form>
<div class="bar">
<input type="search" id="q" data-i18n-placeholder="search" placeholder="Buscar…" aria-label="Buscar" />
</div>
<div id="list"><p class="empty" data-i18n="loading">Cargando…</p></div>
</main>
<footer><span data-i18n="footer">Página interna · no indexada</span> · <span id="count">—</span></footer>
<script>
const API = 'https://n8n.odoo4projects.com/webhook/oca-py/faq';
const el = (id) => document.getElementById(id);
const I18N = {
es: {
title: 'Preguntas frecuentes', subtitle: 'OCA Paraguay · uso interno', badge: 'no indexada',
search: 'Buscar pregunta, área o respuesta…', loading: 'Cargando…', empty: 'Sin resultados.',
footer: 'Página interna · no indexada', count: (n, total) => n + ' de ' + total + ' preguntas',
group: 'General', errRead: 'No se pudo leer el webhook',
errData: 'El webhook respondió sin datos',
hint: 'En n8n, poné el nodo Webhook en "Respond: When Last Node Finishes".',
loginTitle: 'Acceso restringido', user: 'Usuario', pass: 'Contraseña', enter: 'Entrar',
badCred: 'Usuario o contraseña incorrectos.',
},
en: {
title: 'Frequently asked questions', subtitle: 'OCA Paraguay · internal use', badge: 'not indexed',
search: 'Search question, area or answer…', loading: 'Loading…', empty: 'No results.',
footer: 'Internal page · not indexed', count: (n, total) => n + ' of ' + total + ' questions',
group: 'General', errRead: 'Could not read the webhook',
errData: 'The webhook returned no data',
hint: 'In n8n, set the Webhook node to "Respond: When Last Node Finishes".',
loginTitle: 'Restricted access', user: 'User', pass: 'Password', enter: 'Sign in',
badCred: 'Wrong user or password.',
}
};
function store(key, val) {
try { localStorage.setItem(key, val); } catch (e) { /* private mode / blocked storage */ }
}
function stored(key) {
try { return localStorage.getItem(key); } catch (e) { return null; }
}
const state = {
lang: ((stored('ocaFaqLang') || (navigator.language || 'es').slice(0, 2)) === 'en') ? 'en' : 'es',
items: [],
};
const t = (k) => I18N[state.lang][k] ?? k;
function applyLang() {
document.documentElement.lang = state.lang;
document.querySelectorAll('[data-i18n]').forEach(n => { n.textContent = t(n.dataset.i18n); });
document.querySelectorAll('[data-i18n-placeholder]').forEach(n => { n.placeholder = t(n.dataset.i18nPlaceholder); });
document.querySelectorAll('.lang button').forEach(b => b.classList.toggle('on', b.dataset.lang === state.lang));
render();
}
function flash(text) {
const m = el('msg');
m.textContent = text;
m.className = 'msg show err';
}
function hideMsg() { el('msg').className = 'msg'; }
function normalize(payload) {
if (Array.isArray(payload)) {
if (payload.length && Array.isArray(payload[0].data)) return payload[0].data;
if (payload.length && payload[0].id !== undefined) return payload;
}
if (payload && Array.isArray(payload.data)) return payload.data;
return [];
}
// Optional per-language variants: Question_en / Answer_es / area_en …
function pick(it, field) {
const want = (field + '_' + state.lang).toLowerCase();
const key = Object.keys(it).find(k => k.toLowerCase() === want);
if (key && it[key]) return it[key];
return it[field] ?? '';
}
// One field, two languages: "texto ES\n=== EN ===\ntext EN"
const LANG_SPLIT = /\n\s*===\s*EN\s*===\s*\n/i;
function content(it, field) {
const raw = pick(it, field);
if (typeof raw !== 'string' || !LANG_SPLIT.test(raw)) return raw;
const parts = raw.split(LANG_SPLIT);
return (state.lang === 'en' ? parts[1] : parts[0]) || parts[0] || '';
}
// Optional language tag on an item (lang / language / idioma)
function itemLang(it) {
const key = Object.keys(it).find(k => ['lang', 'language', 'idioma'].includes(k.toLowerCase()));
const v = key ? String(it[key] || '').toLowerCase().slice(0, 2) : '';
return (v === 'es' || v === 'en') ? v : '';
}
function render() {
const list = el('list');
if (!state.items.length) {
list.innerHTML = '<p class="empty">' + t('empty') + '</p>';
el('count').textContent = t('count')(0, 0);
return;
}
const term = (el('q').value || '').trim().toLowerCase();
const shown = state.items.filter(it => {
const lang = itemLang(it);
if (lang && lang !== state.lang) return false;
if (!term) return true;
return [pick(it, 'area'), pick(it, 'Question'), pick(it, 'Answer')]
.join(' ').toLowerCase().includes(term);
});
el('count').textContent = t('count')(shown.length, state.items.length);
list.innerHTML = '';
if (!shown.length) { list.innerHTML = '<p class="empty">' + t('empty') + '</p>'; return; }
const groups = {};
shown.forEach(it => {
const area = pick(it, 'area') || t('group');
(groups[area] = groups[area] || []).push(it);
});
Object.keys(groups).sort().forEach(area => {
const sec = document.createElement('section');
sec.className = 'area';
const h = document.createElement('h2');
h.textContent = area;
sec.appendChild(h);
groups[area].forEach(it => {
const card = document.createElement('details');
card.className = 'card';
const sum = document.createElement('summary');
sum.textContent = content(it, 'Question') || '—';
const ans = document.createElement('p');
ans.className = 'ans';
ans.textContent = content(it, 'Answer') || '';
card.append(sum, ans);
sec.appendChild(card);
});
list.appendChild(sec);
});
}
const AUTH_KEY = 'ocaFaqAuth';
function sGet(k) { try { return sessionStorage.getItem(k); } catch (e) { return null; } }
function sSet(k, v) { try { sessionStorage.setItem(k, v); } catch (e) {} }
function sDel(k) { try { sessionStorage.removeItem(k); } catch (e) {} }
function showAuth(on, bad) {
el('auth').hidden = !on;
document.querySelector('.bar').hidden = on;
el('list').hidden = on;
el('auth-err').textContent = (on && bad) ? t('badCred') : '';
el('count').textContent = '';
if (on) setTimeout(() => el('au').focus(), 60);
}
async function load() {
const cred = sGet(AUTH_KEY);
if (!cred) { showAuth(true); return; }
try {
const res = await fetch(API, {
headers: { Accept: 'application/json', Authorization: 'Basic ' + cred },
});
if (res.status === 401) { sDel(AUTH_KEY); showAuth(true, true); return; }
if (!res.ok) throw new Error('HTTP ' + res.status);
const payload = await res.json();
state.items = normalize(payload);
showAuth(false);
hideMsg();
render();
if (!state.items.length && !Array.isArray(payload)) {
flash(t('errData') + ' (' + JSON.stringify(payload) + '). ' + t('hint'));
}
} catch (err) {
state.items = [];
render();
flash(t('errRead') + ' (' + err.message + ').');
}
}
const langBox = document.querySelector('.lang');
if (langBox) {
langBox.addEventListener('click', (ev) => {
const b = ev.target.closest('button[data-lang]');
if (!b) return;
state.lang = b.dataset.lang;
store('ocaFaqLang', state.lang);
applyLang();
});
}
// Accordion: only one answer open at a time
el('list').addEventListener('toggle', (ev) => {
const d = ev.target;
if (!d || d.tagName !== 'DETAILS' || !d.open) return;
el('list').querySelectorAll('details[open]').forEach(o => { if (o !== d) o.open = false; });
}, true);
el('q').addEventListener('input', render);
el('auth').addEventListener('submit', (ev) => {
ev.preventDefault();
const u = el('au').value.trim();
const p = el('ap').value;
if (!u) { el('au').focus(); return; }
try { sSet(AUTH_KEY, btoa(u + ':' + p)); } catch (e) { sSet(AUTH_KEY, ''); }
el('ap').value = '';
load();
});
applyLang();
load();
</script>
</body>
</html>