← back to Gated Queue Runner

index.html

324 lines

<!doctype html>
<html lang="en">
<head>
<script>
// Credential-safe fetch guard (fleet drop-in) — if this page is opened with
// credentials in the URL (a saved bookmark / Chrome-remembered basic-auth:
// https://user:pass@host/…), the browser poisons document.baseURI, so any
// relative or root-relative fetch('/api/…') throws "Request cannot be constructed
// from a URL that includes credentials" and callers silently fall to an empty
// state. Resolve every non-absolute request URL against the credential-free
// location instead of baseURI. Placed first so it wraps window.fetch before any
// app script runs. Ref: creds-in-url-fetch-guard-fleet-pattern.
(function () {
  var _fetch = window.fetch.bind(window);
  var cleanBase = function () { return location.origin + location.pathname; };
  window.fetch = function (input, init) {
    try {
      if (typeof input === 'string' && !/^[a-z]+:\/\//i.test(input) && input.indexOf('//') !== 0) {
        input = new URL(input, cleanBase()).href;
      } else if (input instanceof Request && !/^[a-z]+:\/\//i.test(input.url)) {
        input = new Request(new URL(input.url, cleanBase()).href, input);
      }
    } catch (_) { /* fall through to native */ }
    return _fetch(input, init);
  };
})();
</script>

<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Things Waiting for Steve to Say Yes</title>
<style>
  :root { --cols: 3; --fs: 16px; }
  * { box-sizing: border-box; }
  body { margin: 0; font-family: -apple-system, "Segoe UI", system-ui, sans-serif; background: #f4f6fb; color: #1c2333; font-size: var(--fs); }
  header { position: sticky; top: 0; z-index: 5; background: #fff; border-bottom: 2px solid #e6eaf2; padding: 14px 18px; }
  h1 { margin: 0 0 4px; font-size: 1.5em; }
  .sub { color: #667; font-size: .85em; }
  .controls { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-top: 10px; }
  .controls label { font-size: .8em; color: #556; display: flex; align-items: center; gap: 5px; }
  select, input[type=search] { padding: 6px 9px; border: 1px solid #ccd3e0; border-radius: 8px; font-size: .95em; }
  input[type=range] { vertical-align: middle; }
  .chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
  .chip { border: 1px solid #d3dae8; background: #fff; border-radius: 999px; padding: 5px 12px; font-size: .82em; cursor: pointer; user-select: none; }
  .chip.on { background: #2d5bff; color: #fff; border-color: #2d5bff; }
  .chip .n { opacity: .7; margin-left: 4px; }
  main { padding: 16px 18px; }
  .grid { display: grid; grid-template-columns: repeat(var(--cols), 1fr); gap: 12px; }
  .card { background: #fff; border: 1px solid #e6eaf2; border-radius: 14px; padding: 14px; display: flex; flex-direction: column; gap: 8px; box-shadow: 0 1px 3px rgba(20,30,60,.05); }
  .card.sel { outline: 3px solid #2d5bff; outline-offset: -1px; }
  .card .pick { font-size: .72em; color: #2d5bff; font-weight: 700; display: flex; align-items: center; gap: 3px; cursor: pointer; margin-right: auto; }
  .card .pick input { width: 16px; height: 16px; }
  .card .top { display: flex; align-items: center; gap: 9px; }
  .card .emoji { font-size: 1.8em; line-height: 1; }
  .card .cat { font-size: .72em; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: #2d5bff; }
  .card .note { font-size: 1.05em; line-height: 1.45; color: #1c2333; }
  .card .when { font-size: .72em; color: #8a93a8; display: flex; align-items: center; gap: 4px; }
  .card .file { font-size: .68em; color: #aab; word-break: break-all; }
  .card button { align-self: flex-start; margin-top: 2px; border: 1px solid #ccd3e0; background: #f7f9ff; border-radius: 8px; padding: 5px 10px; font-size: .8em; cursor: pointer; }
  table { width: 100%; border-collapse: collapse; background: #fff; font-size: .9em; }
  thead th { position: sticky; top: 0; background: #eef2fb; text-align: left; padding: 8px 10px; border-bottom: 2px solid #d3dae8; cursor: pointer; user-select: none; white-space: nowrap; }
  thead th:hover { background: #e2e8f8; }
  thead th .ar { color: #2d5bff; margin-left: 4px; }
  tbody td { padding: 8px 10px; border-bottom: 1px solid #eef1f7; vertical-align: top; }
  tbody tr:hover { background: #f7f9ff; }
  tbody tr.sel { background: #eaf0ff; }
  td.about { max-width: 380px; }
  td.effect { color: #445; max-width: 260px; font-size: .92em; }
  td.file { font-size: .78em; color: #99a; word-break: break-all; max-width: 220px; }
  td .link { color: #2d5bff; cursor: pointer; text-decoration: underline; font-size: .85em; }
  .listtbl tbody td { padding: 4px 10px; font-size: .88em; }
  .listtbl tr.cpt td { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 480px; }
  .ttl { font-weight: 600; }
  dialog { border: none; border-radius: 14px; max-width: 760px; width: 92vw; padding: 0; }
  dialog .dh { padding: 12px 16px; border-bottom: 1px solid #eee; font-weight: 700; display: flex; justify-content: space-between; }
  dialog pre { margin: 0; padding: 16px; white-space: pre-wrap; font-size: .82em; max-height: 70vh; overflow: auto; }
  dialog .x { cursor: pointer; border: none; background: #eee; border-radius: 8px; padding: 3px 10px; }
  /* ---------- day / night ---------- */
  .themebtn { border:1px solid #ccd3e0; background:#fff; border-radius:8px; padding:6px 10px; cursor:pointer; font-size:.85em; }
  html.dark body { background:#0f1420; color:#e3e8f2; }
  html.dark header { background:#161c2b; border-bottom-color:#263149; }
  html.dark .sub { color:#8a93a8; }
  html.dark .controls label { color:#9aa4bb; }
  html.dark select, html.dark input[type=search], html.dark .themebtn,
  html.dark #allshown, html.dark .card button { background:#1c2436; border-color:#2c3650; color:#e3e8f2; }
  html.dark .chip { background:#1c2436; border-color:#2c3650; color:#cdd5e5; }
  html.dark .chip.on { background:#2d5bff; color:#fff; border-color:#2d5bff; }
  html.dark .card { background:#161c2b; border-color:#263149; box-shadow:none; }
  html.dark .card .note { color:#e3e8f2; }
  html.dark .card .when { color:#7c869c; }
  html.dark .card .file { color:#66708a; }
  html.dark table { background:#161c2b; }
  html.dark thead th { background:#1c2436; color:#cdd5e5; border-bottom-color:#2c3650; }
  html.dark thead th:hover { background:#232d44; }
  html.dark tbody td { border-bottom-color:#232d44; }
  html.dark tbody tr:hover { background:#1c2436; }
  html.dark tbody tr.sel { background:#22305a; }
  html.dark td.effect { color:#9aa4bb; }
  html.dark td.file { color:#66708a; }
  html.dark dialog { background:#161c2b; color:#e3e8f2; }
  html.dark dialog .dh { border-bottom-color:#263149; }
  html.dark dialog pre { color:#cdd5e5; }
  html.dark dialog .x { background:#2c3650; color:#e3e8f2; }
</style>
</head>
<body>
<header>
  <h1>🚦 Things waiting for you to say YES</h1>
  <div class="sub" id="sub">loading…</div>
  <div class="controls">
    <input type="search" id="q" placeholder="🔎 search…">
    <label>View
      <select id="view"><option value="cards">🃏 Cards</option><option value="list">📋 List</option><option value="table">▦ Table</option></select>
    </label>
    <label>Sort
      <select id="sort">
        <option value="priority">🔥 Priority (top first)</option>
        <option value="value">💰 Biggest value</option>
        <option value="urgency">⏰ Most urgent</option>
        <option value="new">Newest first</option>
        <option value="old">Oldest first</option>
        <option value="cat">By kind</option>
        <option value="az">A → Z</option>
      </select>
    </label>
    <label>Size <input type="range" id="dens" min="1" max="5" value="3"></label>
    <button id="allshown" style="border:1px solid #ccd3e0;background:#fff;border-radius:8px;padding:6px 10px;cursor:pointer;font-size:.85em">☑ select all shown</button>
  <button id="theme" class="themebtn" title="day / night">🌙 Night</button>
  </div>
  <div class="chips" id="chips"></div>
</header>
<main>
  <div class="grid" id="grid"></div>
  <table id="tbl" style="display:none"><thead id="thead"></thead><tbody id="tbody"></tbody></table>
  <table id="ltbl" class="listtbl" style="display:none"><thead id="lhead"></thead><tbody id="lbody"></tbody></table>
</main>
<div id="selbar" style="display:none;position:fixed;bottom:0;left:0;right:0;background:#1c2333;color:#fff;padding:12px 18px;align-items:center;gap:10px;flex-wrap:wrap;box-shadow:0 -2px 10px rgba(0,0,0,.25);z-index:20">
  <span id="selcount" style="font-weight:700;margin-right:6px"></span>
  <button class="act" data-k="allow" style="background:#1f9d55;color:#fff;border:none;border-radius:8px;padding:9px 16px;font-size:.95em;cursor:pointer">✅ Allow (do later)</button>
  <button class="act" data-k="run" style="background:#2d5bff;color:#fff;border:none;border-radius:8px;padding:9px 16px;font-size:.95em;cursor:pointer">▶️ Run now (iTerm2)</button>
  <button class="act" data-k="never" style="background:#b23b3b;color:#fff;border:none;border-radius:8px;padding:9px 16px;font-size:.95em;cursor:pointer">🚫 Never run (archive)</button>
  <button id="savebtn" style="background:#39415a;color:#fff;border:none;border-radius:8px;padding:9px 14px;cursor:pointer">💾 Save list</button>
  <button id="clearbtn" style="background:#39415a;color:#fff;border:none;border-radius:8px;padding:9px 12px;cursor:pointer">clear</button>
  <span id="saveok" style="color:#7CFFB0"></span>
</div>
<dialog id="dlg"><div class="dh"><span id="dt"></span><button class="x" onclick="dlg.close()">close ✕</button></div><pre id="dp"></pre></dialog>
<script>
const KID = { spend:'💵', send:'✉️', dns:'🌐', shopify:'🛍️', catalog:'📦', deploy:'🚀', ga4:'📊', blocked:'🔒', other:'📄' };
let DATA = { gates: [], counts: {}, total: 0 };
let active = new Set(); // active category filters (empty = all)
let SEL = new Set(JSON.parse(localStorage.gqSel || '[]'));
const $ = id => document.getElementById(id);
function saveSel(){ localStorage.gqSel = JSON.stringify([...SEL]); }
function bar(){
  const b=$('selbar');
  if(SEL.size){ b.style.display='flex'; $('selcount').textContent=`✅ ${SEL.size} picked`; }
  else b.style.display='none';
}
$('savebtn').onclick=()=>{
  fetch('/api/select',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({files:[...SEL]})})
    .then(r=>r.json()).then(d=>{ $('saveok').textContent=`saved ${d.count} — tell Claude "do my list"`; setTimeout(()=>$('saveok').textContent='',6000); });
};
$('clearbtn').onclick=()=>{ SEL.clear(); saveSel(); bar(); render(); };
$('allshown').onclick=()=>{ getRows().forEach(x=>SEL.add(x.file)); saveSel(); bar(); render(); };
document.querySelectorAll('.act').forEach(btn=>btn.onclick=()=>{
  const k=btn.dataset.k, files=[...SEL];
  if(!files.length) return;
  const verb={allow:'ALLOW (move to the do-later pile)', run:`RUN NOW — this opens ${files.length} iTerm2 window${files.length>1?'s':''}`, never:'NEVER RUN (archive out of the queue)'}[k];
  if(!confirm(`${verb}\n\n${files.length} item${files.length>1?'s':''}. Continue?`)) return;
  if(k==='run' && files.length>10 && !confirm(`That is ${files.length} windows at once. Really open all of them?`)) return;
  $('saveok').textContent='working…';
  fetch('/api/'+k,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({files})})
    .then(r=>r.json()).then(d=>{
      $('saveok').textContent = k==='run' ? `opened ${d.done} window(s)` : `${k==='allow'?'allowed':'archived'} ${d.done}`;
      if(k!=='run'){ files.forEach(f=>SEL.delete(f)); saveSel(); }   // acted items leave the queue
      else { files.forEach(f=>SEL.delete(f)); saveSel(); }
      setTimeout(()=>{ $('saveok').textContent=''; load(); }, 1200);   // reload list (acted items gone)
    }).catch(()=>{ $('saveok').textContent='error'; });
});

function fmtWhen(ms){ return new Date(ms).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); }

// day / night
function applyTheme(dark){ document.documentElement.classList.toggle('dark',dark); $('theme').textContent = dark?'☀️ Day':'🌙 Night'; localStorage.gqTheme = dark?'dark':'light'; }
$('theme').onclick = ()=> applyTheme(!document.documentElement.classList.contains('dark'));
applyTheme((localStorage.gqTheme||(matchMedia&&matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'))==='dark');

function load(){
  fetch('/api/gates').then(r=>r.json()).then(d=>{
    // flatten ratings so every field is a sortable top-level column
    (d.gates||[]).forEach(g=>{ const r=g.ratings||{}; g.rval=r.value; g.rurg=r.urgency; g.rease=r.ease; g.rsafe=r.safety; });
    DATA=d; $('sub').textContent = `${d.total} things need your OK. Check the boxes for the ones you want done, then Save.`;
    renderChips(); render(); bar();
  });
}
function renderChips(){
  const c=$('chips'); c.innerHTML='';
  const cats = Object.entries(DATA.counts).sort((a,b)=>b[1]-a[1]);
  cats.forEach(([k,n])=>{
    const el=document.createElement('span');
    el.className='chip'+(active.has(k)?' on':'');
    el.innerHTML=`${KID[k]||'📄'} ${k}<span class="n">${n}</span>`;
    el.onclick=()=>{ active.has(k)?active.delete(k):active.add(k); renderChips(); render(); };
    c.appendChild(el);
  });
}
let tSort={field:'created',dir:-1};
function getRows(){
  const q=$('q').value.toLowerCase();
  return DATA.gates.filter(x=> (active.size===0||active.has(x.category)) &&
    (!q || (x.about+x.effect+x.file+x.title+x.catLabel).toLowerCase().includes(q)));
}
function sortByCol(g){
  const key={kind:'catLabel',pick:'file',pri:'priority'}[tSort.field]||tSort.field;
  g.sort((a,b)=>{let va=a[key],vb=b[key]; if(typeof va==='string'){va=va.toLowerCase();vb=(vb||'').toLowerCase();} return (va<vb?-1:va>vb?1:0)*tSort.dir;});
  return g;
}
function render(){
  const view=$('view').value; let g=getRows();
  $('grid').style.display='none'; $('tbl').style.display='none'; $('ltbl').style.display='none';
  if(view==='table'){ $('tbl').style.display=''; renderTable(sortByCol(g)); }
  else if(view==='list'){ $('ltbl').style.display=''; renderList(sortByCol(g)); }
  else {
    $('grid').style.display='';
    const s=$('sort').value;
    if(s==='new')g.sort((a,b)=>b.created-a.created); else if(s==='old')g.sort((a,b)=>a.created-b.created);
    else if(s==='az')g.sort((a,b)=>a.title.localeCompare(b.title)); else if(s==='cat')g.sort((a,b)=>a.category.localeCompare(b.category)||b.created-a.created);
    else if(s==='value')g.sort((a,b)=>(b.ratings.value-a.ratings.value)||(b.money-a.money)||b.priority-a.priority);
    else if(s==='urgency')g.sort((a,b)=>(b.ratings.urgency-a.ratings.urgency)||((a.days??999)-(b.days??999))||b.priority-a.priority);
    else g.sort((a,b)=>b.priority-a.priority||b.created-a.created); // priority (default)
    renderCards(g);
  }
}
const TIER={high:{c:'#c0392b',bg:'#fdecea',l:'🔴 High'},med:{c:'#b8860b',bg:'#fdf6e3',l:'🟡 Medium'},low:{c:'#5a6a85',bg:'#eef2fb',l:'⚪ Low'}};
function dots(n){ let h=''; for(let i=1;i<=5;i++) h+=`<span style="display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:2px;background:${i<=n?'currentColor':'#e3e8f2'}"></span>`; return h; }
function ratingRow(emoji,label,n,color){ return `<div style="display:flex;align-items:center;gap:6px;font-size:.72em;color:#556"><span style="width:74px;color:#667">${emoji} ${label}</span><span style="color:${color};line-height:1">${dots(n)}</span></div>`; }
function ratingsHtml(x){
  const money = x.money>0 ? `<span class="pill" style="background:#eafaf0;color:#1f7a44;border:1px solid #b7e4c7">💰 $${x.money>=1e6?(x.money/1e6).toFixed(x.money>=1e7?0:1)+'M':x.money>=1e3?(x.money/1e3).toFixed(0)+'k':x.money}</span>`:'';
  const dl = x.days!=null ? `<span class="pill" style="background:${x.days<=3?'#fdecea':'#fdf6e3'};color:${x.days<=3?'#c0392b':'#8a6d1a'};border:1px solid #eee">⏰ ${x.days<=0?'due today':'in '+x.days+'d'}</span>`:'';
  return `<div style="display:flex;gap:6px;flex-wrap:wrap;margin:2px 0 4px">${money}${dl}</div>
    <div style="display:flex;flex-direction:column;gap:3px">
      ${ratingRow('💰','value',x.ratings.value,'#1f9d55')}
      ${ratingRow('⏰','urgency',x.ratings.urgency,'#c0392b')}
      ${ratingRow('⚡','easy',x.ratings.ease,'#2d5bff')}
      ${ratingRow('✅','safe',x.ratings.safety,'#7a5cff')}
    </div>`;
}
function renderCards(g){
  const grid=$('grid'); grid.innerHTML='';
  g.forEach(x=>{
    const d=document.createElement('div'); d.className='card'; const on=SEL.has(x.file);
    const t=TIER[x.tier]||TIER.low;
    d.innerHTML=`<div class="top"><label class="pick"><input type="checkbox" ${on?'checked':''}> pick</label>
        <span title="priority rank" style="font-weight:800;font-size:.82em;color:#fff;background:${t.c};border-radius:7px;padding:2px 7px">#${x.rank}</span>
        <span class="emoji">${x.emoji}</span><span class="cat">${x.catLabel}</span>
        <span style="margin-left:auto;font-size:.7em;font-weight:700;color:${t.c};background:${t.bg};border-radius:999px;padding:2px 9px">${t.l}</span></div>
      <div class="note">${esc(x.note)}</div>
      ${ratingsHtml(x)}
      <div class="when">🕓 ${fmtWhen(x.created)}</div>
      <div class="file">${esc(x.file)}</div>
      <button>read the grown-up version →</button>`;
    if(on) d.classList.add('sel');
    d.querySelector('input').onchange=e=>{ e.target.checked?SEL.add(x.file):SEL.delete(x.file); d.classList.toggle('sel',e.target.checked); saveSel(); bar(); };
    d.querySelector('button').onclick=()=>openMemo(x.file,x.title);
    grid.appendChild(d);
  });
  if(!g.length) grid.innerHTML='<p style="color:#889">Nothing matches.</p>';
}
// full column set for TABLE; compact subset for LIST — every column is click-to-sort
const COLS_TABLE=[{f:'pick',l:'✔'},{f:'rank',l:'#'},{f:'pri',l:'Priority'},{f:'rval',l:'💰Val'},{f:'rurg',l:'⏰Urg'},{f:'rease',l:'⚡Ease'},{f:'rsafe',l:'✅Safe'},{f:'kind',l:'Kind'},{f:'title',l:'Title'},{f:'about',l:"What it's about"},{f:'effect',l:'If you say YES'},{f:'created',l:'When'},{f:'size',l:'Size'},{f:'file',l:'File'}];
const COLS_LIST =[{f:'pick',l:'✔'},{f:'rank',l:'#'},{f:'pri',l:'Priority'},{f:'kind',l:'Kind'},{f:'title',l:'Title'},{f:'created',l:'When'}];
function cellFor(f,x){
  const t=(typeof TIER!=='undefined'&&TIER[x.tier])||{c:'#5a6a85',bg:'#eef2fb',l:'⚪ Low'};
  switch(f){
    case 'pick': return `<input type="checkbox" ${SEL.has(x.file)?'checked':''}>`;
    case 'rank': return `<span style="font-weight:800;color:#fff;background:${t.c};border-radius:6px;padding:1px 6px">#${x.rank}</span>`;
    case 'pri': return `<span style="white-space:nowrap;font-weight:700;color:${t.c}">${t.l}</span> <span style="color:#aab;font-size:.85em">${x.priority}</span>`;
    case 'rval': return `<b>${x.rval??''}</b>`;
    case 'rurg': return `<b>${x.rurg??''}</b>`;
    case 'rease': return `<b>${x.rease??''}</b>`;
    case 'rsafe': return `<b>${x.rsafe??''}</b>`;
    case 'kind': return `<span style="white-space:nowrap">${x.emoji} ${esc(x.catLabel)}</span>`;
    case 'title': return `<span class="ttl">${esc(x.title)}</span>`;
    case 'about': return esc(x.about);
    case 'effect': return esc(x.effect);
    case 'created': return `<span style="white-space:nowrap">${fmtWhen(x.created)}</span>`;
    case 'size': return (x.size/1024).toFixed(1)+' KB';
    case 'file': return `${esc(x.file)} <span class="link">open</span>`;
  }
  return '';
}
function renderGrid(g, cols, tblId, compact){
  const th=$(tblId+'head'), tb=$(tblId+'body');
  th.innerHTML='<tr>'+cols.map(c=>{const ar=tSort.field===c.f?`<span class="ar">${tSort.dir>0?'▲':'▼'}</span>`:'';return `<th data-f="${c.f}">${c.l}${ar}</th>`;}).join('')+'</tr>';
  th.querySelectorAll('th').forEach(t=>t.onclick=()=>{const f=t.dataset.f; if(tSort.field===f)tSort.dir*=-1; else{tSort.field=f;tSort.dir=(f==='created'?-1:1);} render();});
  tb.innerHTML='';
  g.forEach(x=>{
    const tr=document.createElement('tr'); if(SEL.has(x.file))tr.className='sel'; if(compact)tr.className+=' cpt';
    tr.innerHTML=cols.map(c=>{
      const cls = c.f==='about'?' class="about"':c.f==='effect'?' class="effect"':c.f==='file'?' class="file"':'';
      return `<td${cls}>${cellFor(c.f,x)}</td>`;
    }).join('');
    const cb=tr.querySelector('input[type=checkbox]'); if(cb) cb.onchange=e=>{ e.target.checked?SEL.add(x.file):SEL.delete(x.file); tr.classList.toggle('sel',e.target.checked); saveSel(); bar(); };
    const lk=tr.querySelector('.link'); if(lk) lk.onclick=()=>openMemo(x.file,x.title);
    tb.appendChild(tr);
  });
  if(!g.length) tb.innerHTML=`<tr><td colspan="${cols.length}" style="color:#889">Nothing matches.</td></tr>`;
}
function renderTable(g){ renderGrid(g, COLS_TABLE, 't', false); }
function renderList(g){ renderGrid(g, COLS_LIST, 'l', true); }
function openMemo(f,t){ fetch('/api/memo?f='+encodeURIComponent(f)).then(r=>r.text()).then(txt=>{ $('dt').textContent=t; $('dp').textContent=txt; $('dlg').showModal(); }); }
function esc(s){ return (s||'').replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c])); }

// controls + persistence
['q','sort','view'].forEach(id=>$(id).addEventListener('input',()=>{save();render();}));
$('dens').addEventListener('input',e=>{ const v=+e.target.value; document.documentElement.style.setProperty('--cols', 6-v); document.documentElement.style.setProperty('--fs', (12+v*2)+'px'); localStorage.gqDens=v; });
function save(){ localStorage.gqSort=$('sort').value; localStorage.gqQ=$('q').value; localStorage.gqView=$('view').value; }
(function restore(){ if(localStorage.gqSort)$('sort').value=localStorage.gqSort; if(localStorage.gqQ)$('q').value=localStorage.gqQ; if(localStorage.gqView)$('view').value=localStorage.gqView;
  const v=localStorage.gqDens||3; $('dens').value=v; document.documentElement.style.setProperty('--cols',6-v); document.documentElement.style.setProperty('--fs',(12+v*2)+'px'); })();
load();
</script>
</body>
</html>