Fix viewer: safe JSON embed so search cards render again.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-26 10:51:37 +03:00
parent a31c8d678c
commit 1c7874a31e
2 changed files with 45 additions and 24 deletions

View File

@@ -216,12 +216,14 @@ def viewer(
home: Optional[str] = Query(None),
):
sections = fetch_sections(prefix)
# Escape < so </script> inside text_html cannot break the page script tag
sections_json = json.dumps(sections, ensure_ascii=False, default=str).replace("<", "\\u003c")
home_url = "/home-image" if (home or HOME_IMAGE or (BASE_DIR.parent / "Bairaci.png").exists()) else None
return templates.TemplateResponse(
request,
"viewer.html",
{
"sections_json": json.dumps(sections, ensure_ascii=False, default=str),
"sections_json": sections_json,
"section_count": len(sections),
"prefix": prefix or "",
"home_url": home_url,

View File

@@ -335,21 +335,44 @@
<div id="toast"></div>
<script type="application/json" id="sections-data">{{ sections_json | safe }}</script>
<script>
const ALL = {{ sections_json | safe }};
let ALL = [];
try {
ALL = JSON.parse(document.getElementById('sections-data').textContent || '[]');
} catch (e) {
console.error('sections JSON parse failed', e);
ALL = [];
}
document.getElementById('total-count').textContent = ALL.length + ' секции';
renderEditor(ALL);
renderKwCloud();
doSearch();
renderGenerator();
function esc(s) {
return String(s ?? '')
.replace(/&/g,'&amp;').replace(/</g,'&lt;')
.replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function shortPath(p) {
if (!p) return '';
const parts = String(p).replace(/\\/g,'/').split('/');
return parts.slice(-2).join('/');
}
function toast(msg, isErr) {
const el = document.getElementById('toast');
if (!el) return;
el.textContent = msg;
el.classList.toggle('error', !!isErr);
el.classList.add('show');
clearTimeout(toast._t);
toast._t = setTimeout(() => el.classList.remove('show'), 2800);
}
function switchTab(name) {
const order = [{% if home_url %}'home',{% endif %}'editor','search','generator'];
document.querySelectorAll('.tab').forEach((t,i) => t.classList.toggle('active', order[i] === name));
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
document.getElementById('tab-' + name).classList.add('active');
const panel = document.getElementById('tab-' + name);
if (panel) panel.classList.add('active');
if (name === 'generator') renderGenerator();
if (name === 'search') doSearch();
}
async function startRescan() {
@@ -703,28 +726,24 @@ ${bodyAbs}
</body></html>`;
}
// ── UTILS ─────────────────────────────
function esc(s) {
return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function shortPath(p) {
if (!p) return '';
const parts = p.replace(/\\/g,'/').split('/');
return parts.slice(-2).join('/');
}
function toast(msg, isError) {
const el = document.getElementById('toast');
el.textContent = msg;
el.classList.toggle('error', !!isError);
el.classList.add('show');
setTimeout(() => el.classList.remove('show'), 3000);
}
function download(filename, content, mime) {
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([content], {type: mime}));
a.download = filename;
a.click();
}
// ── INIT ─────────────────────────────
document.getElementById('total-count').textContent = ALL.length + ' секции';
try {
renderEditor(ALL);
renderKwCloud();
doSearch();
renderGenerator();
} catch (e) {
console.error('init failed', e);
toast('Грешка при зареждане на UI: ' + e.message, true);
}
</script>
</body>
</html>