Et valgfrit fælles link, /podcast/f/<nøgle>/, til fx familiens Messenger- gruppe: samme afspillerside som den personlige, med abonnér-knapper og feed under samme sti, men uden login og uden admin-ting. Kladder vises aldrig der. I /admin/podcast laves linket, nøglen kan skiftes (det gamle dør med det samme), det kan slås fra, og hentningerne tælles. Siden er noindex (header og meta) og sender ingen referrer. Forkerte nøgler giver 404 ligesom ukendte stier, og en klient der rammer 20 forkerte nøgler på ti minutter, får 404 på alt under /podcast/<nøgle> i et stykke tid. Det gælder også de personlige nøgler. Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
120 lines
6.7 KiB
HTML
120 lines
6.7 KiB
HTML
<!doctype html>
|
|
<html lang="da" data-theme="dark">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<meta name="theme-color" content="#0b0f17" media="(prefers-color-scheme: dark)" />
|
|
<meta name="theme-color" content="#f6f8fb" media="(prefers-color-scheme: light)" />
|
|
<link rel="icon" href="/static/icons/icon.svg" type="image/svg+xml" />
|
|
<title>{% block title %}Weircon{% endblock %}</title>
|
|
<!-- no-flash: set theme before any stylesheet paints (key 'theme', shared with the theme JS) -->
|
|
<script>(()=>{try{var t=localStorage.getItem('theme')||(matchMedia('(prefers-color-scheme: light)').matches?'light':'dark');document.documentElement.dataset.theme=t}catch(e){document.documentElement.dataset.theme='dark'}})()</script>
|
|
<!-- shared design system (SSOT: agw/Weircon-wind) hosted on assets.weircon.dk -->
|
|
<link rel="stylesheet" href="https://assets.weircon.dk/css/weirsoe-theme.css" />
|
|
<link rel="stylesheet" href="/static/portal.css" />
|
|
<script defer src="https://assets.weircon.dk/js/weirsoe-theme.js"></script>
|
|
{% block head %}{% endblock %}
|
|
</head>
|
|
<body>
|
|
<header class="wt-header">
|
|
<nav class="wt-nav">
|
|
<a class="wt-brand" href="/">weircon<span class="wt-brand-dot">.</span></a>
|
|
<div class="wt-nav-links">
|
|
<a class="wt-nav-link" href="/">Forside</a>
|
|
{% if is_admin %}<a class="wt-nav-link" href="/admin">Admin</a>{% endif %}
|
|
{% if user %}<span class="who">{{ user.name }}</span> <a class="wt-nav-link" href="/auth/logout">Log ud</a>{% endif %}
|
|
<button type="button" data-theme-toggle class="theme-toggle" title="Skift mellem lyst og mørkt tema" aria-label="Skift tema">
|
|
<svg class="sun-and-moon" aria-hidden="true" width="22" height="22" viewBox="0 0 24 24">
|
|
<mask class="moon" id="moon-mask"><rect x="0" y="0" width="100%" height="100%" fill="white" /><circle cx="24" cy="10" r="6" fill="black" /></mask>
|
|
<circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
|
|
<g class="sun-beams" stroke="currentColor">
|
|
<line x1="12" y1="1" x2="12" y2="3" /><line x1="12" y1="21" x2="12" y2="23" />
|
|
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" /><line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
|
<line x1="1" y1="12" x2="3" y2="12" /><line x1="21" y1="12" x2="23" y2="12" />
|
|
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" /><line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
|
</g>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</nav>
|
|
</header>
|
|
<main class="wt-main">
|
|
{% block body %}{% endblock %}
|
|
</main>
|
|
<footer class="wt-footer">
|
|
<div class="wt-footer-inner">
|
|
Driftet af Asger Weirsøe · self-hosted på Proxmox · <a href="mailto:asger@weirsoe.dk">asger@weirsoe.dk</a>
|
|
</div>
|
|
</footer>
|
|
|
|
<!-- ⌘K / Ctrl+K kommandopalette -->
|
|
<div id="palette" class="wt-palette" hidden>
|
|
<div class="wt-palette-box" role="dialog" aria-modal="true" aria-label="Kommandopalette">
|
|
<input id="palette-input" class="wt-palette-input" type="text" placeholder="Søg tjenester, sider eller kommandoer…" autocomplete="off" spellcheck="false" />
|
|
<ul id="palette-list" class="wt-palette-list" role="listbox"></ul>
|
|
<div class="wt-palette-hint">↑↓ navigér · ↵ vælg · esc luk · ⌘/Ctrl K åbn</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
(function () {
|
|
if (window.__palette) return; window.__palette = true;
|
|
var index = null, items = [], sel = 0;
|
|
var staticActions = [
|
|
{ type:'action', title:'Tema: mørkt', run:function(){ setTheme('dark'); } },
|
|
{ type:'action', title:'Tema: lyst', run:function(){ setTheme('light'); } },
|
|
{ type:'action', title:'Log ud', url:'/auth/logout' },
|
|
];
|
|
function setTheme(t){ document.documentElement.dataset.theme = t; localStorage.setItem('theme', t); }
|
|
function el(id){ return document.getElementById(id); }
|
|
function ensureIndex(){
|
|
if (index) return Promise.resolve();
|
|
return fetch('/api/search-index').then(function(r){ return r.json(); })
|
|
.then(function(j){ index = j; }).catch(function(){ index = []; });
|
|
}
|
|
function score(q, it){
|
|
var hay = (it.title + ' ' + (it.extra || '')).toLowerCase();
|
|
if (!q) return (it.type === 'page' || it.type === 'action') ? 1 : 0.5;
|
|
if (hay.indexOf(q) === 0) return 3;
|
|
if (hay.indexOf(q) > -1) return 2;
|
|
var i = 0; for (var k = 0; k < hay.length; k++) if (hay[k] === q[i]) i++;
|
|
return i === q.length ? 1 : 0;
|
|
}
|
|
function refresh(){
|
|
var q = el('palette-input').value.trim().toLowerCase();
|
|
items = (index || []).concat(staticActions)
|
|
.map(function(it){ return { it: it, s: score(q, it) }; })
|
|
.filter(function(x){ return x.s > 0; })
|
|
.sort(function(a, b){ return b.s - a.s; })
|
|
.slice(0, 9).map(function(x){ return x.it; });
|
|
sel = 0;
|
|
el('palette-list').innerHTML = items.map(function(it, i){
|
|
var badge = it.type === 'app' ? 'app' : it.type === 'action' ? 'kommando' : it.ext ? 'link' : 'side';
|
|
return '<li class="wt-palette-item' + (i === sel ? ' is-selected' : '') + '" data-i="' + i + '" role="option">' +
|
|
'<span>' + String(it.title).replace(/</g, '<') + '</span><span class="wt-palette-badge">' + badge + '</span></li>';
|
|
}).join('') || '<li class="wt-palette-item wt-palette-empty">Ingen resultater</li>';
|
|
}
|
|
function paint(){ var ls = el('palette-list').querySelectorAll('.wt-palette-item');
|
|
for (var i = 0; i < ls.length; i++) ls[i].classList.toggle('is-selected', i === sel); }
|
|
function choose(i){ var it = items[i]; if (!it) return; close();
|
|
if (it.run) it.run(); else if (it.ext) window.open(it.url, '_blank', 'noopener'); else window.location.href = it.url; }
|
|
function open(){ ensureIndex().then(function(){ el('palette').hidden = false; el('palette-input').value = ''; refresh(); el('palette-input').focus(); }); }
|
|
function close(){ var p = el('palette'); if (p) p.hidden = true; }
|
|
document.addEventListener('keydown', function(e){
|
|
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k'){ e.preventDefault(); el('palette').hidden ? open() : close(); return; }
|
|
var p = el('palette'); if (!p || p.hidden) return;
|
|
if (e.key === 'Escape') close();
|
|
else if (e.key === 'ArrowDown'){ e.preventDefault(); sel = Math.min(sel + 1, items.length - 1); paint(); }
|
|
else if (e.key === 'ArrowUp'){ e.preventDefault(); sel = Math.max(sel - 1, 0); paint(); }
|
|
else if (e.key === 'Enter'){ e.preventDefault(); choose(sel); }
|
|
});
|
|
document.addEventListener('input', function(e){ if (e.target && e.target.id === 'palette-input') refresh(); });
|
|
document.addEventListener('click', function(e){
|
|
if (e.target === el('palette')) close();
|
|
var li = e.target.closest && e.target.closest('.wt-palette-item[data-i]');
|
|
if (li) choose(+li.dataset.i);
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|