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:
2026-10-17 13:18:03 +00:00
parent 33eb1f4db2
commit 9a41641eaa
15 changed files with 242 additions and 71 deletions

View File

@@ -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{