Signals: an admin chooses whether the server's TypeSafe key pays for everyone (#271)
With signals_on_server set and TYPESAFE_KEY present, the judge asks Jev about everyone's Signals on the server's key; otherwise each person turns Signals on in Settings with a key of their own, which is now checked with TypeSafe before it is saved. The Signals folder shows only when they are on. The browser suite's daemon inherited the shell's TYPESAFE_KEY, so fixture feeds were sent to the real TypeSafe to be categorised; it is blanked now, and an empty key counts as none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,11 @@ async function drawServer(){
|
||||
<input type="number" id="gart" min="0" value="${g.art_cache_mb}">
|
||||
<span class="hint">Show and episode artwork is kept here once shown, so it loads from iPX
|
||||
instead of each publisher. Over this, what has gone longest unshown is dropped first.</span></div>
|
||||
<div class="field"><label class="check"><input type="checkbox" id="gsig"${g.signals_on_server ? ' checked' : ''}${g.typesafe_server_key ? '' : ' disabled'}>
|
||||
Signals for everyone, on this server's TypeSafe key</label>
|
||||
<span class="hint">${g.typesafe_server_key
|
||||
? 'On, everyone has Signals and every item read for one is a call on this server\'s key. Off, each person turns Signals on in their own Settings with a key of their own.'
|
||||
: 'This server has no TypeSafe key (<code>TYPESAFE_KEY</code>), so each person uses their own.'}</span></div>
|
||||
<div class="field"><label>Download folder</label>
|
||||
<span class="hint" style="overflow-wrap:anywhere">${esc(g.download_dir)}</span></div>
|
||||
<div class="cardacts"><button class="btn primary" id="gsave">${ICON.check} Save</button></div>`;
|
||||
@@ -64,7 +69,9 @@ async function drawServer(){
|
||||
media_types: $<HTMLInputElement>('#gtypes').value.split(',').map((t: string) => t.trim()).filter(Boolean),
|
||||
max_total_gb: Number($<HTMLInputElement>('#gquota').value) || 0,
|
||||
max_age_days: Number($<HTMLInputElement>('#gage').value) || 0,
|
||||
art_cache_mb: Math.max(0, Number($<HTMLInputElement>('#gart').value) || 0)})});
|
||||
art_cache_mb: Math.max(0, Number($<HTMLInputElement>('#gart').value) || 0),
|
||||
// Left alone while the server has no key to offer, rather than switched off.
|
||||
...($<HTMLInputElement>('#gsig').disabled ? {} : {signals_on_server: $<HTMLInputElement>('#gsig').checked})})});
|
||||
toast('Settings saved');
|
||||
}catch(e){ toast(why(e), true); }
|
||||
};
|
||||
|
||||
@@ -440,13 +440,18 @@ async function prefsModal(){
|
||||
<span class="hint">Comma separated words or phrases, in every feed you read. An item with
|
||||
one in its title or text is hidden from you and not downloaded for you. Each feed's
|
||||
settings can add more.</span></div>
|
||||
<div class="field"><label for="stskey">TypeSafe key</label>
|
||||
<div class="inline"><input type="password" id="stskey" autocomplete="off" spellcheck="false"
|
||||
placeholder="${S.me?.typesafe_key_set?'Saved. Paste a new one to replace it':'Paste your key'}">
|
||||
<button class="btn ico" id="stsforget" title="Forget the key" aria-label="Forget the key"${S.me?.typesafe_key_set?'':' hidden'}>${ICON.trash}</button></div>
|
||||
<span class="hint">Signals pick what matters out of everything you subscribe to, on your own
|
||||
key from <a href="https://typesafe.ai" target="_blank" rel="noopener">typesafe.ai</a>.
|
||||
The key is kept on this server and never shown again.</span></div>
|
||||
${S.me?.signals==='server'?`<div class="field"><label>Signals</label>
|
||||
<span class="hint">Signals pick what matters out of everything you subscribe to. This server
|
||||
pays for them, so there is nothing to set up.</span></div>`:`<div class="field">
|
||||
<label class="check"><input type="checkbox" id="ssigon"${S.me?.typesafe_key_set?' checked':''}> Signals</label>
|
||||
<span class="hint">Signals pick what matters out of everything you subscribe to. To use Signals
|
||||
you will need an API key from <a href="https://typesafe.ai" target="_blank" rel="noopener">typesafe.ai</a>.</span>
|
||||
<div id="ssigkey"${S.me?.typesafe_key_set?'':' hidden'}>
|
||||
<label for="stskey">TypeSafe key</label>
|
||||
<div class="inline"><input type="password" id="stskey" autocomplete="off" spellcheck="false"
|
||||
placeholder="${S.me?.typesafe_key_set?'Saved. Paste a new one to replace it':'Paste your key'}">
|
||||
<button class="btn ico" id="stsforget" title="Forget the key" aria-label="Forget the key"${S.me?.typesafe_key_set?'':' hidden'}>${ICON.trash}</button></div>
|
||||
<span class="hint">Checked with TypeSafe before it is saved, kept on this server and never shown again.</span></div></div>`}
|
||||
<div class="field boxed"><label for="tokname">API tokens</label>
|
||||
<div id="tokens"></div>
|
||||
<div class="inline"><input type="text" id="tokname" placeholder="What it is for">
|
||||
@@ -476,18 +481,31 @@ async function prefsModal(){
|
||||
$<HTMLInputElement>('#tokval').select();
|
||||
}catch(err){ toast(why(err),true); }
|
||||
};
|
||||
const saveKey=async(typesafe_key: string|null)=>{
|
||||
try{
|
||||
await api('/api/me',{method:'PATCH',body:JSON.stringify({typesafe_key})});
|
||||
if(S.me) S.me.typesafe_key_set=!!typesafe_key;
|
||||
$<HTMLInputElement>('#stskey').value='';
|
||||
$<HTMLInputElement>('#stskey').placeholder=typesafe_key?'Saved. Paste a new one to replace it':'Paste your key';
|
||||
$<HTMLButtonElement>('#stsforget').hidden=!typesafe_key;
|
||||
toast(typesafe_key?'Saved':'Forgotten');
|
||||
}catch(err){ toast(why(err),true); }
|
||||
};
|
||||
$<HTMLInputElement>('#stskey').onchange=(e: Event)=>{ const k=(e.target as HTMLInputElement).value.trim(); if(k) saveKey(k); };
|
||||
$<HTMLButtonElement>('#stsforget').onclick=()=>saveKey(null);
|
||||
// Your own key, unless the server's is used for everyone (#271). Signals are on once TypeSafe
|
||||
// has taken a key, and off again when it is forgotten.
|
||||
if(S.me?.signals!=='server'){
|
||||
const saveKey=async(typesafe_key: string|null)=>{
|
||||
try{
|
||||
await api('/api/me',{method:'PATCH',body:JSON.stringify({typesafe_key})});
|
||||
if(S.me) S.me.typesafe_key_set=!!typesafe_key;
|
||||
$<HTMLInputElement>('#stskey').value='';
|
||||
$<HTMLInputElement>('#stskey').placeholder=typesafe_key?'Saved. Paste a new one to replace it':'Paste your key';
|
||||
$<HTMLButtonElement>('#stsforget').hidden=!typesafe_key;
|
||||
$<HTMLInputElement>('#ssigon').checked=!!typesafe_key; $('#ssigkey').hidden=!typesafe_key;
|
||||
toast(typesafe_key?'Signals are on':'Signals are off');
|
||||
renderFeeds();
|
||||
}catch(err){ toast(why(err),true); }
|
||||
};
|
||||
$<HTMLInputElement>('#ssigon').onchange=(e: Event)=>{
|
||||
const on=(e.target as HTMLInputElement).checked;
|
||||
if(on){ $('#ssigkey').hidden=false; $<HTMLInputElement>('#stskey').focus(); return; }
|
||||
if(!S.me?.typesafe_key_set){ $('#ssigkey').hidden=true; return; }
|
||||
if(confirm('Turn Signals off and forget your TypeSafe key? Your signals are kept for when you turn them on again.')) saveKey(null);
|
||||
else (e.target as HTMLInputElement).checked=true;
|
||||
};
|
||||
$<HTMLInputElement>('#stskey').onchange=(e: Event)=>{ const k=(e.target as HTMLInputElement).value.trim(); if(k) saveKey(k); };
|
||||
$<HTMLButtonElement>('#stsforget').onclick=()=>$<HTMLInputElement>('#ssigon').click();
|
||||
}
|
||||
$<HTMLInputElement>('#sblock').onchange=async (e: Event)=>{
|
||||
const blocked=splitWords((e.target as HTMLInputElement).value);
|
||||
try{
|
||||
|
||||
@@ -18,6 +18,10 @@ function signalOf(view: string|null){
|
||||
return id===null ? undefined : S.signals.find(s=>s.id===id);
|
||||
}
|
||||
|
||||
/// Whether you have Signals (#271): everyone does on the server's key; on your own, once you have
|
||||
/// turned them on in Settings with a key TypeSafe took.
|
||||
const signalsOn=()=>S.me?.signals==='server'||!!S.me?.typesafe_key_set;
|
||||
|
||||
/// The Signals folder: first in the feed list, under All Subscriptions, open or closed as an OPML's
|
||||
/// is (#255, #256). As rows under the places, a long list of them pushed the feeds down.
|
||||
const SIGNALS=':signals';
|
||||
@@ -42,6 +46,7 @@ function signalTile(sg: {name: string, icon: string|null}){
|
||||
}
|
||||
const badgeHTML=(n: number)=>`<span class="badge${n?'':' zero'}" title="${n} unread">${n>999?'999+':n}</span>`;
|
||||
function signalFolder(list: HTMLElement, q: string){
|
||||
if(!signalsOn()) return;
|
||||
const mine=S.signals.filter(s=>!q||s.name.toLowerCase().includes(q));
|
||||
if(q&&!mine.length) return;
|
||||
const open=expanded.has(SIGNALS)||!!q;
|
||||
@@ -101,7 +106,7 @@ function renderSignals(){
|
||||
}
|
||||
|
||||
function signalHead(sg: Signal){
|
||||
const nokey=S.me&&!S.me.typesafe_key_set;
|
||||
const nokey=S.me&&!signalsOn();
|
||||
return `
|
||||
<div class="fhead slim">
|
||||
${signalTile(sg)}
|
||||
@@ -130,7 +135,7 @@ async function signalAction(a: string, sg: Signal){
|
||||
|
||||
/// Making a Signal, or changing one.
|
||||
function signalModal(sg?: Signal){
|
||||
const nokey=S.me&&!S.me.typesafe_key_set;
|
||||
const nokey=S.me&&!signalsOn();
|
||||
openModal(`<h3>${sg?'Edit signal':'New signal'}</h3>
|
||||
<div class="field"><label for="sgname">Name</label>
|
||||
<div class="inline"><button type="button" class="iconpick" id="sgicon" title="Choose an icon" aria-label="Choose an icon"
|
||||
@@ -148,8 +153,7 @@ function signalModal(sg?: Signal){
|
||||
<input type="text" id="sgdesc" value="${esc(sg?.description||'')}" placeholder="AI tools for writing software: coding agents, vibe coding">
|
||||
<span class="hint">Every item in the feeds you subscribe to is read, the last 30 days of them
|
||||
and each new one, and the ones about this subject are pulled out: its name, these words and
|
||||
your sentence together. The rest stays noise, in its own feed. Each item is one call on your
|
||||
TypeSafe key.${sg?' A change here reads everything again.':''}</span></div>
|
||||
your sentence together. The rest stays noise, in its own feed.${S.me?.signals==='server'?'':' Each item is one call on your TypeSafe key.'}${sg?' A change here reads everything again.':''}</span></div>
|
||||
${nokey?`<p class="hint" style="color:var(--warn)">Without a TypeSafe key, nothing is picked out. ${KEY_LINKS}</p>`:''}
|
||||
<div class="cardacts"><button class="btn ico" onclick="closeModal()" title="Cancel" aria-label="Cancel">${ICON.close}</button>
|
||||
<button class="btn ico primary" id="sgsave" title="${sg?'Save':'Make signal'}" aria-label="${sg?'Save':'Make signal'}">${sg?ICON.check:ICON.plus}</button></div>`);
|
||||
@@ -280,6 +284,7 @@ const KEY_LINKS=`<a href="https://typesafe.ai" target="_blank" rel="noopener">Ge
|
||||
async function keySetting(){
|
||||
closeModal();
|
||||
await prefsModal();
|
||||
$<HTMLInputElement>('#ssigon').checked=true; $('#ssigkey').hidden=false;
|
||||
const k=$<HTMLInputElement>('#stskey'); k.scrollIntoView({block:'center'}); k.focus();
|
||||
}
|
||||
document.addEventListener('click',(e: MouseEvent)=>{
|
||||
|
||||
@@ -84,6 +84,8 @@ interface Me {
|
||||
version: string;
|
||||
/// A TypeSafe key is saved for Signals; the key itself never comes back.
|
||||
typesafe_key_set: boolean;
|
||||
/// Whose TypeSafe key Signals are judged on (#271): the server's, for everyone, or each person's own.
|
||||
signals: 'server' | 'own';
|
||||
}
|
||||
|
||||
/// A Signal in your sidebar, `/api/signals` (db.rs SignalRow).
|
||||
|
||||
Reference in New Issue
Block a user