shaiwatcher/assets/docs/cmd.html
Franz Rolfsvaag 1ede582a76 0.4.2.0.a1
- DD cycle data fetching
  - ShaiWatcher will now keep an updated loot table of the unique items in the DD each week
    The bot will **only** edit its message if already present, which should reduce message spam
  - Added command `/dd_update` to control the update behaviour. stop|resume|start [reason_text]
- Docsite changes
  - Added "ADMIN" tags to commands, signifying owner-only commands
  - Owner-only commands are now filtered under the "moderator" category
  - Added docs for `/dd_update`
- Logging
  - Added logging info for more verbose info relating to configuration and installation
2025-08-16 06:39:01 +02:00

569 lines
26 KiB
HTML
Raw Permalink 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="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width">
<!-- Favicons (put files in /assets/docs/) -->
<link rel="icon" type="image/png" href="/assets/docs/favicon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/docs/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/docs/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/docs/apple-touch-icon.png">
<link rel="manifest" href="/assets/docs/site.webmanifest">
<title>__TITLE__</title>
<style>
:root { --bg:#0b0f14; --panel:#121922; --muted:#6b7280; --fg:#e5e7eb; --accent:#60a5fa; --sticky-top: 8px; --header-h: 56px; }
* { box-sizing:border-box; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Inter, Arial; }
body { margin:0; background:var(--bg); color:var(--fg); }
/* Header: 2/3 title, 1/3 actions */
header { padding:16px 20px; background:#0f172a; border-bottom:1px solid #223; position:sticky; top:0; z-index:3; }
.hdr { display:flex; align-items:center; gap:12px; }
.hdr .title { flex: 2 1 66%; font-size:20px; font-weight:600; }
.hdr .actions { flex: 1 1 34%; display:flex; justify-content:flex-end; gap:8px; }
main { max-width:1200px; margin:20px auto; padding:0 16px 40px; }
.row { display:flex; gap:16px; flex-wrap:wrap; }
.col { flex:1 1 560px; min-width:320px; }
.panel { background:var(--panel); border:1px solid #1f2937; border-radius:12px; padding:16px; }
.toolbar { margin-bottom:16px; position:sticky; top: var(--header-h); z-index:2; }
.search { width:100%; padding:10px 12px; border-radius:8px; border:1px solid #1f2937; background:#0b1220; color:var(--fg); }
.list { margin-top:12px; display:flex; flex-direction:column; gap:10px; transition: filter .2s ease; }
.card { border:1px solid #233; border-radius:10px; padding:10px 12px; background:#0c1522; cursor:default; scroll-margin-top: calc(var(--sticky-top) + 12px); }
.name { font-weight:600; display:flex; align-items:center; gap:8px; }
.meta { font-size:12px; color:var(--muted); display:flex; gap:10px; flex-wrap:wrap; margin-top:4px; }
.pill { display:inline-block; padding:2px 8px; border-radius:999px; font-size:12px; border:1px solid #2b4; }
.pill.mod { border-color:#ef4444; color:#fecaca; }
.pill.admin { border-color:#a78bfa; color:#e9d5ff; }
.pill.slash { border-color:#60a5fa; }
.pill.prefix { border-color:#f59e0b; }
.pill.hybrid { border-color:#34d399; }
/* Usage block + copy button */
.usage{
--lh:1.6; /* line-height used to size the button */
line-height:var(--lh);
position:relative;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size:12px;
background:#0a1220;
border:1px dashed #1f2937;
padding:6px 40px 6px 8px; /* right padding to make room for the button */
border-radius:6px; margin-top:6px; word-break:break-word;
}
.usage > .copybtn{
position:absolute; right:0; top:0;
height: calc(var(--lh) * 1em + 12px); /* one text line + vertical padding */
width:36px;
display:flex; align-items:center; justify-content:center;
background:#374151; border:0; color:#e5e7eb; border-radius:0 6px 0 6px;
cursor:pointer;
}
.usage > .copybtn:hover{ background:#4b5563; }
@media (max-width: 900px){ .usage > .copybtn{ width:auto; padding:0 10px; } }
.help { margin-top:6px; color:#cbd5e1; }
.btn { padding:4px 8px; border:1px solid #334155; border-radius:8px; background:#0b1220; color:#e5e7eb; cursor:pointer; font-size:12px; }
.btn:hover { background:#0f172a; }
.btn-icon { width:28px; height:28px; display:inline-flex; align-items:center; justify-content:center; padding:0; }
.btn-row { display:flex; gap:8px; align-items:center; }
.detailsbox { margin-top:12px; position: sticky; top: var(--sticky-top); min-height: 280px; z-index:1; transition: transform .25s ease; }
/* Footer */
footer { margin-top:16px; color:var(--muted); font-size:12px; text-align:center; }
footer .line { margin:4px 0; }
footer a { color: var(--fg); text-decoration: none; border-bottom: 1px dotted #334155; }
footer a:hover { text-decoration: underline; }
/* Discord drawer (right-side sheet) */
.drawer{ position:fixed; top:var(--header-h); right:0; width:clamp(360px,33vw,420px);
height:calc(100vh - var(--header-h) - 16px); background:var(--panel); border-left:1px solid #1f2937;
box-shadow:-16px 0 40px rgba(0,0,0,.45); transform:translateX(100%); transition:transform .25s ease;
z-index:6; border-top-left-radius:12px; border-bottom-left-radius:12px; overflow:hidden; }
.drawer.open{ transform:translateX(0); }
.drawer-close{ position:absolute; top:8px; right:8px; z-index:1; }
.drawer-backdrop{ position:fixed; inset:var(--header-h) 0 0 0; background:rgba(0,0,0,.35); display:none; z-index:5; }
.drawer-backdrop.open{ display:block; }
/* Blur veil under toolbar */
.veil{ position:sticky; top:var(--header-h); height:16px; pointer-events:none; z-index:2;
backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px);
mask-image:linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,1));
-webkit-mask-image:linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,1)); }
/* Mobile details as sheet */
@media (max-width: 900px){
.row{ display:block; }
.detailsbox{ position:fixed; right:0; top:var(--header-h); width:96vw; max-width:720px;
height:calc(100vh - var(--header-h) - 16px); overflow:auto; transform:translateX(100%);
box-shadow:-12px 0 30px rgba(0,0,0,.4); z-index:4; }
.detailsbox.open{ transform:translateX(0); }
#backdrop{ position:fixed; inset:var(--header-h) 0 0 0; background:rgba(0,0,0,.35); display:none; z-index:3; }
#backdrop.open{ display:block; }
#list.blur{ filter:blur(2px); }
#backdrop::before{ content:""; position:absolute; left:10px; top:50%; transform:translateY(-50%);
font-size:48px; line-height:1; color:#e5e7eb; opacity:.8; }
}
/* Anchored copy popover + backdrop */
#copyBackdrop{ position:fixed; inset:0; display:none; z-index:20; background:transparent; }
#copyBackdrop.open{ display:block; }
#copyPopover{ position:fixed; display:none; z-index:21; background:var(--panel); border:1px solid #1f2937;
border-radius:10px; padding:10px; min-width: min(420px, 90vw);
box-shadow:0 18px 50px rgba(0,0,0,.45); }
#copyPopover.open{ display:block; }
#copyPopover .title{ font-weight:600; margin-bottom:6px; }
#copyPopover pre{ margin:6px 0; white-space:pre-wrap; word-break:break-word; background:#0a1220; border:1px dashed #1f2937; padding:8px; border-radius:6px; }
/* Tiny toast */
#toast{ position:fixed; left:50%; bottom:28px; transform:translateX(-50%); background:#111827; color:#e5e7eb;
border:1px solid #1f2937; padding:6px 10px; border-radius:999px; font-size:12px; opacity:0; pointer-events:none; z-index:40; transition:opacity .18s ease; }
#toast.show{ opacity:1; }
/* Bottom sheet (full command details) */
#fullDetailsBackdrop{
position:fixed; inset:var(--header-h) 0 0 0; display:none; z-index:12;
background:rgba(0,0,0,.35);
}
#fullDetailsBackdrop.open{ display:block; }
#fullDetailsSheet{
position:fixed; left:50%; transform:translate(-50%, 100%);
bottom:0; width:min(1200px, 94vw); max-height:75vh; overflow:auto;
background:var(--panel); border:1px solid #1f2937;
border-radius:12px 12px 0 0; box-shadow:0 18px 50px rgba(0,0,0,.45);
z-index:13; transition:transform .25s ease;
}
#fullDetailsSheet.open{ transform:translate(-50%, 0); }
#fullDetailsHead{
position:sticky; top:0; background:rgba(0,0,0,.35);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
display:flex; align-items:center; gap:8px;
padding:10px 12px; border-bottom:1px solid #1f2937;
}
#fullDetailsTitle{ font-weight:600; }
#fullDetailsBody{ padding:12px; }
/* Small inline flag in footer */
.flag-emoji{
height: 1em;
width: auto;
vertical-align: -0.18em;
display: inline-block;
border-radius: 2px;
}
</style>
</head>
<body>
<header>
<div class="hdr">
<div class="title">__TITLE__</div>
<div class="actions">
<button id="openDiscord" class="btn btn-icon" title="Open Discord">💬</button>
</div>
</div>
</header>
<main>
<!-- Toolbar -->
<div class="panel toolbar" id="toolbar">
<input id="q" class="search" placeholder="Search name/description…">
<div style="margin-top:8px; font-size:12px; color:var(--muted)">
Sections: <a href="#user">User</a> · <a href="#moderator">Moderator</a> · <a href="#all">All</a>
<span id="counts" style="margin-left:10px"></span>
<span id="alerts" style="margin-left:10px; color:#fbbf24"></span>
</div>
</div>
<div class="veil" id="veil"></div>
<div class="row">
<div class="col"><div id="list" class="list"></div></div>
<div class="col"><div class="panel detailsbox" id="details"></div></div>
</div>
<footer id="footer">
<div class="line" id="copyright"></div>
<div class="line" id="statusline">Uptime: — · Version: v—</div>
<div class="line" id="coffee" style="display:__SUPPORT_VIS__">
<a id="supportLink" href="__SUPPORT_URL__" target="_blank" rel="noopener noreferrer">__SUPPORT_LABEL__</a>
</div>
</footer>
<!-- Mobile details backdrop -->
<div id="backdrop"></div>
<!-- Discord drawer -->
<div id="discordBackdrop" class="drawer-backdrop"></div>
<div id="discordSheet" class="drawer">
<button class="btn btn-icon drawer-close" title="Close"></button>
<iframe id="discordWidget"
src="https://discord.com/widget?id=1396826999095427253&theme=dark"
allowtransparency="true" frameborder="0"
sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts"
style="width:100%;height:100%;border:0"></iframe>
</div>
<!-- Anchored copy popover -->
<div id="copyBackdrop"></div>
<div id="copyPopover" role="dialog" aria-modal="true">
<div class="title">Copy to clipboard</div>
<pre id="copyText"></pre>
<div class="btn-row">
<button id="copyAction" class="btn">Copy</button>
<button id="copyClose" class="btn">Close</button>
</div>
</div>
<!-- Bottom full-details sheet -->
<div id="fullDetailsBackdrop"></div>
<div id="fullDetailsSheet" role="dialog" aria-modal="true" aria-labelledby="fullDetailsTitle">
<div id="fullDetailsHead">
<div id="fullDetailsTitle">Command details</div>
<span style="flex:1"></span>
<button id="fullDetailsClose" class="btn btn-icon" title="Close"></button>
</div>
<div id="fullDetailsBody"></div>
</div>
<!-- Tiny toast -->
<div id="toast" aria-live="polite">Copied!</div>
</main>
<script>
/* Sticky offsets */
function computeStickyTop(){
const header=document.querySelector('header');
const toolbar=document.getElementById('toolbar');
const headerH=header?header.offsetHeight:0;
document.documentElement.style.setProperty('--header-h', headerH+'px');
const toolbarH=toolbar?toolbar.offsetHeight:0;
const stickyTop=headerH+toolbarH+8;
document.documentElement.style.setProperty('--sticky-top', stickyTop+'px');
const veil=document.getElementById('veil');
if(veil){ veil.style.top=headerH+'px'; veil.style.height='16px'; }
}
addEventListener('resize', computeStickyTop);
addEventListener('load', computeStickyTop);
/* Error surface */
addEventListener('error', e=>{ const el=document.getElementById('alerts'); if(el) el.textContent='JS error: '+(e?.message||''); });
addEventListener('unhandledrejection', e=>{ const el=document.getElementById('alerts'); const msg=e&&(e.reason&&(e.reason.message||e.reason)||e); if(el) el.textContent='Promise error: '+msg; });
/* Mini MD (fallback when only .md exists) */
function renderMD(src){ if(!src) return ""; let s=String(src).replace(/\r\n/g,"\n");
s=s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
s=s.replace(/```([\s\S]*?)```/g,(m,p1)=>"<pre><code>"+p1+"</code></pre>");
s=s.replace(/^###\s+(.*)$/gm,"<h3>$1</h3>");
s=s.replace(/^##\s+(.*)$/gm,"<h2>$1</h2>");
s=s.replace(/^#\s+(.*)$/gm,"<h1>$1</h1>");
s=s.replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>");
s=s.replace(/\*(.+?)\*/g,"<em>$1</em>");
s=s.replace(/`([^`]+?)`/g,"<code>$1</code>");
s=s.replace(/^(\s*)-\s+(.+)$/gm,"$1<li>$2</li>");
s=s.replace(/(<li>.*<\/li>\n?)+/g,m=>"<ul>"+m+"</ul>");
s=s.replace(/^(?!<h\d|<ul>|<pre>|<li>)([^\n][^\n]*)$/gm,"<p>$1</p>");
return s; }
/* Link helpers */
function rowAnchor(r){ const cog=(r.cog||'nocog').toLowerCase();
let base=(r.name||'').toLowerCase().replace(/^\//,'').replace(/\s+/g,'-').replace(/\//g,'-'); return `${cog}-${base}`; }
function getFilterFromHash(){ const h=(location.hash||'#user').slice(1);
const parts=h.split('&').map(s=>s.trim()); const filt=parts.find(p=>p==='user'||p==='moderator'||p==='all')||'user';
let cmd=null; const cmdPart=parts.find(p=>p.startsWith('cmd=')); if(cmdPart) cmd=cmdPart.slice(4);
const sp=new URLSearchParams(location.search); const qcmd=sp.get('cmd'); if(!cmd&&qcmd) cmd=qcmd; return {filter:filt, cmd}; }
function buildLink(anchor){ const url=new URL(location.href); url.searchParams.set('cmd',anchor); url.hash=location.hash||'#user'; return url.toString(); }
function replaceURLFor(anchor){ try{ const url=new URL(location.href); url.searchParams.set('cmd',anchor); history.replaceState(null,'',url.toString()); }catch{} }
/* Copy helpers */
async function copyText(s){ try{ await navigator.clipboard.writeText(s); return true; }catch{ return false; } }
function showToast(msg='Copied!'){ const t=document.getElementById('toast'); t.textContent=msg; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'), 1100); }
const copyBackdrop=document.getElementById('copyBackdrop');
const copyPopover=document.getElementById('copyPopover');
const copyTextEl=document.getElementById('copyText');
const copyAction=document.getElementById('copyAction');
const copyClose=document.getElementById('copyClose');
function closeCopyPopover(){ copyBackdrop.classList.remove('open'); copyPopover.classList.remove('open'); }
copyBackdrop.addEventListener('click', closeCopyPopover);
copyClose.addEventListener('click', closeCopyPopover);
function openCopyPopover(text, anchorEl){
copyTextEl.textContent=text;
copyAction.onclick=async ()=>{ const ok=await copyText(text); if(ok){ closeCopyPopover(); showToast(); } };
// Position: prefer above; keep inside viewport
const r=anchorEl.getBoundingClientRect();
const pad=8, vw=innerWidth, vh=innerHeight;
copyPopover.style.visibility='hidden'; copyPopover.classList.add('open'); // measure
const pw=copyPopover.offsetWidth, ph=copyPopover.offsetHeight;
let left = Math.min(Math.max(pad, r.right - pw), vw - pw - pad);
let top = r.top - ph - pad;
if(top < (pad + 4)) top = Math.min(vh - ph - pad, r.bottom + pad);
copyPopover.style.left = `${left}px`;
copyPopover.style.top = `${top}px`;
copyPopover.style.visibility='visible';
copyBackdrop.classList.add('open');
}
/* Bottom sheet */
const fdb = document.getElementById('fullDetailsBackdrop');
const fds = document.getElementById('fullDetailsSheet');
const fdbtn = document.getElementById('fullDetailsClose');
const fdbody = document.getElementById('fullDetailsBody');
const fdtitle = document.getElementById('fullDetailsTitle');
function openFullDetails(title, html){
fdtitle.textContent = title || 'Command details';
fdbody.innerHTML = html || '<p>No extra details.</p>';
fdb.classList.add('open');
fds.classList.add('open');
}
function closeFullDetails(){ fdb.classList.remove('open'); fds.classList.remove('open'); }
fdb?.addEventListener('click', closeFullDetails);
fdbtn?.addEventListener('click', closeFullDetails);
/* App */
(function(){
if(!location.hash) location.hash='#user';
let data=(window.__DATA__||null);
const listEl=document.getElementById('list');
const qEl=document.getElementById('q');
const countsEl=document.getElementById('counts');
const detailsEl=document.getElementById('details');
const backdrop=document.getElementById('backdrop');
function shownName(r){ return (r.display_name||r.name||'').replace(/^\//,''); }
function helpSansMod(r){ return (r.help||'').replace(/^\s*\[(MOD|ADMIN)\]\s*/i,''); }
function moduleSansPrefix(r){ const m=r.module||''; return m.replace(/^modules?\./,'').replace(/^discord\.ext\./,''); }
async function shareFor(r){
const anchor=rowAnchor(r);
const url=buildLink(anchor);
const ok=await copyText(url);
if(ok) showToast('Link copied!');
else openCopyPopover(url, document.querySelector(`#card-${anchor} [data-share]`) || document.getElementById('shareDetails') || document.body);
}
function usageBlockHTML(text){
if(!text) return '';
const esc=String(text).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
return `<div class="usage">${esc}<button class="btn btn-icon copybtn" title="Copy usage" data-copy="${esc}">📋</button></div>`;
}
function wireUsageCopy(container){
container.querySelectorAll('[data-copy]').forEach(btn=>{
btn.addEventListener('click', async ev=>{
ev.stopPropagation();
const t=btn.getAttribute('data-copy').replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&amp;/g,'&');
const ok=await copyText(t);
if(ok) showToast();
else openCopyPopover(t, btn);
});
});
}
function openDetails(r){
const anchor=rowAnchor(r);
replaceURLFor(anchor);
let html=`
<div class="name" style="margin-bottom:6px">
<span class="pill ${r.type}">${r.type}</span>
${r.moderator_only?'<span class="pill mod">mod</span>':''}
${r.admin_only?'<span class="pill admin">admin</span>':''}
<span>${shownName(r)}</span>
<span style="flex:1"></span>
<div class="btn-row">
<button class="btn btn-icon" title="Copy link" id="shareDetails">🔗</button>
<button class="btn" id="closeDetails" style="display:none">Close</button>
</div>
</div>
<div class="meta">
${r.cog?`<span>cog: ${r.cog}</span>`:''}
${r.module?`<span>module: ${moduleSansPrefix(r)}</span>`:''}
<span>runs: ${Number(r.exec_count||0).toLocaleString()}</span>
</div>
${usageBlockHTML(r.usage_prefix)}
${usageBlockHTML(r.usage_slash)}
${helpSansMod(r)?`<div class="help">${helpSansMod(r)}</div>`:''}
`;
// Brief/details HTML (or MD fallback)
if (r.brief_html) {
html += `<div class="md" style="margin-top:10px">${r.brief_html}</div>`;
} else if (r.details_html) {
html += `<div class="md" style="margin-top:10px">${r.details_html}</div>`;
} else if (r.details_md) {
html += `<div class="md" style="margin-top:10px">${renderMD(r.details_md)}</div>`;
} else {
html += `<div class="md" style="margin-top:10px"><p style="color:#9ca3af;font-size:14px;margin:0 0 6px 0">No custom documentation yet.</p></div>`;
}
// "Open full details" when we have long content
const hasFull = !!(r.details_html || r.details_md);
if (hasFull) {
html += `<div class="btn-row" style="margin-top:10px">
<button id="openFullDetails" class="btn">Open full details</button>
</div>`;
}
detailsEl.innerHTML=html;
const shareBtn=document.getElementById('shareDetails');
if(shareBtn) shareBtn.onclick=(e)=>{ e.stopPropagation(); shareFor(r); };
const fullBtn=document.getElementById('openFullDetails');
if(fullBtn){
fullBtn.onclick=(e)=>{
e.stopPropagation();
const longHtml = r.details_html ? r.details_html : renderMD(r.details_md || '');
openFullDetails(shownName(r), longHtml);
};
}
wireUsageCopy(detailsEl);
// Mobile sheet open
if (matchMedia('(max-width: 900px)').matches){
detailsEl.classList.add('open'); listEl.classList.add('blur'); backdrop.classList.add('open');
const closeBtn=document.getElementById('closeDetails'); if(closeBtn) closeBtn.style.display='inline-block';
backdrop.onclick=closeDetails; if(closeBtn) closeBtn.onclick=closeDetails;
}
}
function closeDetails(){ detailsEl.classList.remove('open'); listEl.classList.remove('blur'); backdrop.classList.remove('open'); }
function card(r){
const c=document.createElement('div'); const anchor=rowAnchor(r);
c.className='card'; c.id='card-'+anchor; c.dataset.anchor=anchor;
c.dataset.search=[shownName(r), helpSansMod(r), r.brief, r.usage, r.usage_prefix, r.usage_slash, r.cog, r.module, r.details_md||"", r.brief_html||"", r.details_html||""].join(' ').toLowerCase();
const usageHTML=(r.type==='hybrid') ? `${usageBlockHTML(r.usage_prefix)}${usageBlockHTML(r.usage_slash)}` : `${usageBlockHTML(r.usage)}`;
c.innerHTML=`
<div class="name">
<span class="pill ${r.type}">${r.type}</span>
${r.moderator_only?'<span class="pill mod">mod</span>':''}
${r.admin_only?'<span class="pill admin">admin</span>':''}
<span>${shownName(r)}</span>
<div class="btn-row">
<button class="btn btn-icon" title="Copy link" data-share="1">🔗</button>
<button class="btn" data-details="1">Details</button>
</div>
</div>
<div class="meta">
${r.cog?`<span>cog: ${r.cog}</span>`:''}
${r.module?`<span>module: ${moduleSansPrefix(r)}</span>`:''}
<span>runs: ${Number(r.exec_count||0).toLocaleString()}</span>
</div>
${usageHTML}
${helpSansMod(r)?`<div class="help">${helpSansMod(r)}</div>`:''}
`;
c.querySelector('[data-details]').addEventListener('click', ev=>{ ev.stopPropagation(); openDetails(r); });
c.querySelector('[data-share]').addEventListener('click', async ev=>{
ev.stopPropagation();
const url=buildLink(anchor);
const ok=await copyText(url);
if(ok) showToast('Link copied!');
else openCopyPopover(url, ev.currentTarget);
});
c.addEventListener('click', ()=>openDetails(r));
wireUsageCopy(c);
return c;
}
function render(target, rows){ target.innerHTML=''; rows.forEach(r=>target.appendChild(card(r))); }
function applyFilter(selectAnchorIfPresent=true){
if(!data) return;
const all=data.all||[], mods=(data.sections&&data.sections.moderator)||[], users=(data.sections&&data.sections.user)||[];
countsEl.textContent=`User: ${users.length} · Moderator: ${mods.length} · Total: ${all.length}`;
const q=(qEl.value||'').toLowerCase();
const {filter, cmd}=getFilterFromHash();
const src=filter==='moderator'?mods:(filter==='all'?all:users);
const rows=!q?src:src.filter(r=>([shownName(r), helpSansMod(r), r.brief, r.usage, r.usage_prefix, r.usage_slash, r.cog, r.module, r.details_md||"", r.brief_html||"", r.details_html||""].join(' ').toLowerCase().includes(q)));
render(listEl, rows);
const isMobile = matchMedia('(max-width: 900px)').matches;
// Selection logic:
// - If URL has an anchor, prefer that (scroll to it)
// - Otherwise, ONLY auto-open first row on non-mobile
let sel = null;
if (selectAnchorIfPresent && cmd) {
const hit = rows.find(r => rowAnchor(r) === cmd);
if (hit) sel = hit;
const cardEl = document.getElementById('card-'+cmd);
if (cardEl) cardEl.scrollIntoView({behavior:'smooth', block:'start'});
} else if (!isMobile && rows.length) {
sel = rows[0];
}
// Do not auto-open on mobile
if (sel && !isMobile) openDetails(sel);
}
async function boot(){
computeStickyTop();
try{
async function loadData(){
try{
const res = await fetch('/api/commands', { cache:'no-store' });
if(!res.ok) throw new Error('HTTP '+res.status);
data = await res.json();
return true; // fetched fresh
}catch(e){
// fallback to inline bootstrap if fetch fails
if(window.__DATA__){ data = window.__DATA__; return false; }
throw e;
}
}
addEventListener('hashchange', ()=>applyFilter(false));
qEl.addEventListener('input', ()=>applyFilter(false));
await loadData(); // always refresh from backend
applyFilter(true);
}catch{ document.getElementById('alerts').textContent='Failed to load.'; }
// Footer
const fromYear=2025, now=new Date();
const flagSvg='<img class="flag-emoji" alt="NO" src="/assets/docs/no.svg">';
document.getElementById('copyright').innerHTML=`© OokamiKunTV ${fromYear}${now.getFullYear()} — Made in ${flagSvg} with ❤️`;
try{
const s=await fetch('/api/status',{cache:'no-store'});
if(s.ok){ const js=await s.json();
document.getElementById('statusline').textContent=`Uptime: ${fmtDuration(js.uptime_seconds||0)} · Version: v${js.version||'unknown'}`; }
}catch{}
// Discord drawer
const ds=document.getElementById('discordSheet');
const db=document.getElementById('discordBackdrop');
const openBtn=document.getElementById('openDiscord');
const closeBtn=ds.querySelector('.drawer-close');
const openDiscord=()=>{ ds.classList.add('open'); db.classList.add('open'); };
const closeDiscord=()=>{ ds.classList.remove('open'); db.classList.remove('open'); };
if(openBtn) openBtn.onclick=openDiscord; if(closeBtn) closeBtn.onclick=closeDiscord; if(db) db.onclick=closeDiscord;
}
function fmtDuration(s){ s=Math.max(0,Math.floor(+s||0));
const d=Math.floor(s/86400); s%=86400; const h=Math.floor(s/3600); s%=3600; const m=Math.floor(s/60); const sec=s%60;
const parts=[]; if(d) parts.push(d+'d'); if(h||d) parts.push(h+'h'); if(m||h||d) parts.push(m+'m'); parts.push(sec+'s'); return parts.join(' '); }
setInterval(async ()=>{
try{
const gotFresh = await loadData();
if(gotFresh) applyFilter(false);
}catch{}
}, 30000);
boot();
})();
</script>
</body>
</html>