feat(beacon): sig det uopfordret, og lad være med at fejle i tavshed
Beaconet er slået til som udgangspunkt. Det kan kun forsvares, hvis det bliver sagt af sig selv — og det blev det ikke. Det stod rigtigt og fyldestgørende under Indstillinger → Data, som er en side, der bliver åbnet af dem, der i forvejen har en mistanke om, at der er noget at læse. For alle andre var "du kan slå den fra" et tilbud til nogen, der ikke vidste, der var noget at slå fra. Nu møder en administrator beskeden første gang: hvad der bliver sendt, med de rigtige værdier i — det rigtige id, den rigtige version, den rigtige adresse — og to knapper. Værdierne står der frem for ordet "anonymt", fordi et løfte om telemetri er præcis så meget værd som læserens mulighed for at efterprøve det. `keep` er en peger og ikke en bool. En tom krop måtte ikke kunne betyde "slå det fra": et fravalg, ingen har truffet, er lige så meget et brudt løfte som et tilvalg, ingen har truffet. Og tre ting mere, som Yggdrasil har lært på den hårde måde: Et beacon, der ikke kan nå sin collector, fejlede i fuldstændig tavshed. Det prøvede igen i morgen, siden sagde "sidst sendt" med en dato, der bare blev ældre, og ingen opdagede det. Grunden bliver nu skrevet ned og vist. En installation, der opdaterer sig selv om natten, meldte gårsdagens version i op til et døgn, fordi døgnet blev talt fra sidste vellykkede melding. En version, der er skiftet, sender nu med det samme. Og det offentliggjorte tal har fået et gulv. Et tal, der falder, kan ikke offentliggøres som en kendsgerning — en dårlig udrulning, en DNS-ændring, der stille stopper meldingerne, og rækker, der er væk, ser alle sammen ud som "færre bruger det nu". Adressen bliver stående: Verdande melder til verdande.yggdrasilpanel.com, ikke til panelets egen collector. De to tæller hver sin slags installation, og en fælles tabel har intet felt til at holde dem ude fra hinanden. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
445d9de57a
commit
0cf5c8c281
@@ -280,6 +280,11 @@ export const api = {
|
||||
|
||||
beacon: () => get('/beacon/settings'),
|
||||
setBeacon: (body) => put('/beacon/settings', body),
|
||||
// Beskeden om, hvad installationen melder ind, og svaret på den. `keep` sendes
|
||||
// altid — en tom krop måtte ikke kunne betyde "slå det fra", og serveren
|
||||
// afviser den derfor.
|
||||
beaconNotice: () => get('/beacon/notice'),
|
||||
answerBeaconNotice: (keep) => post('/beacon/notice', { keep }),
|
||||
|
||||
listBackups: () => get('/backups'),
|
||||
runBackup: () => post('/backups'),
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
<script>
|
||||
/**
|
||||
* Beskeden om, at denne installation melder ind — sagt uopfordret, første gang
|
||||
* en administrator er logget ind.
|
||||
*
|
||||
* Beaconet er slået til som udgangspunkt, og det kan kun forsvares, hvis det
|
||||
* bliver sagt af sig selv. Siden under Indstillinger → Data siger det rigtigt
|
||||
* og fyldestgørende, og den bliver åbnet af dem, der i forvejen har en
|
||||
* mistanke om, at der er noget at læse; for alle andre er "du kan slå den fra"
|
||||
* et tilbud til nogen, der ikke ved, der er noget at slå fra.
|
||||
*
|
||||
* De tre værdier står i beskeden — det rigtige id, den rigtige version, den
|
||||
* rigtige adresse — frem for ordene "anonyme data". Et løfte om telemetri er
|
||||
* præcis så meget værd som læserens mulighed for at efterprøve det.
|
||||
*
|
||||
* Kun til administratorer, og hentet på `app.user` frem for i onMount: en
|
||||
* indlæsning, der kører, mens login-siden står, kører aldrig for den, der lige
|
||||
* er logget ind — og så er beskeden aldrig sagt til dem, den er skrevet til.
|
||||
*/
|
||||
import { app } from '$lib/stores.svelte.js';
|
||||
import { api } from '$lib/api.js';
|
||||
import { t } from '$lib/i18n.svelte.js';
|
||||
import { humanMessage } from '$lib/api.js';
|
||||
|
||||
let notice = $state(null);
|
||||
let busy = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
const me = app.user;
|
||||
if (!me?.is_admin) {
|
||||
notice = null;
|
||||
return;
|
||||
}
|
||||
let alive = true;
|
||||
api
|
||||
.beaconNotice()
|
||||
.then((r) => {
|
||||
if (alive && r?.pending) notice = r;
|
||||
})
|
||||
.catch(() => {
|
||||
// En besked, der ikke kunne hentes, må ikke stå i vejen for appen.
|
||||
// Den er stadig ubesvaret næste gang, og bliver stillet igen.
|
||||
});
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
});
|
||||
|
||||
async function answer(keep) {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
try {
|
||||
await api.answerBeaconNotice(keep);
|
||||
notice = null;
|
||||
app.toast(keep ? t('beacon.noticeKept') : t('beacon.noticeStopped'));
|
||||
} catch (e) {
|
||||
app.toast(humanMessage(e));
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if notice}
|
||||
<aside class="notice" role="region" aria-label={t('beacon.noticeTitle')}>
|
||||
<h2>{t('beacon.noticeTitle')}</h2>
|
||||
<p>{t('beacon.noticeBody')}</p>
|
||||
|
||||
<!-- Det, der bliver sendt, med de rigtige værdier i. Ikke et eksempel: det
|
||||
er selve meldingen, og den kan sammenholdes med indstillingssiden. -->
|
||||
<pre>{JSON.stringify(
|
||||
{ instance_id: notice.instance_id, version: notice.version },
|
||||
null,
|
||||
2
|
||||
)}</pre>
|
||||
<p class="to">{t('beacon.noticeTo', { url: notice.collector_url })}</p>
|
||||
|
||||
<div class="row">
|
||||
<button class="button" disabled={busy} onclick={() => answer(true)}>
|
||||
{t('beacon.noticeKeep')}
|
||||
</button>
|
||||
<button class="secondary" disabled={busy} onclick={() => answer(false)}>
|
||||
{t('beacon.noticeStop')}
|
||||
</button>
|
||||
<a href="/indstillinger/data">{t('beacon.noticeMore')}</a>
|
||||
</div>
|
||||
</aside>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
/* Nederst i hjørnet, ikke hen over skærmen. Beskeden skal ses og besvares —
|
||||
den skal ikke tages som en fejl eller som noget, der spærrer for arbejdet,
|
||||
for så bliver den klikket væk uden at blive læst. */
|
||||
.notice {
|
||||
position: fixed;
|
||||
right: var(--s3);
|
||||
bottom: var(--s3);
|
||||
z-index: 60;
|
||||
width: min(24rem, calc(100vw - 2 * var(--s3)));
|
||||
padding: var(--s3);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-lg, var(--radius));
|
||||
background: var(--surface-raised, var(--surface));
|
||||
box-shadow: var(--shadow-lg, 0 10px 30px rgb(0 0 0 / 0.25));
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.notice h2 {
|
||||
margin: 0 0 var(--s1);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.notice p {
|
||||
margin: 0 0 var(--s2);
|
||||
color: var(--ink-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.notice pre {
|
||||
margin: 0 0 var(--s1);
|
||||
padding: var(--s2);
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface-sunken, var(--surface));
|
||||
font-size: var(--text-xs);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.notice .to {
|
||||
font-size: var(--text-xs);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.row a {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
</style>
|
||||
@@ -90,6 +90,22 @@ export const da = {
|
||||
'beacon.active7': 'Aktive 7 dage',
|
||||
'beacon.active30': 'Aktive 30 dage',
|
||||
'beacon.byVersion': 'Efter version (30 dage):',
|
||||
// Beskeden, der bliver sagt uopfordret første gang. Værdierne står i den, ikke
|
||||
// ordet 'anonymt': et løfte om telemetri er præcis så meget værd som
|
||||
// læserens mulighed for at efterprøve det.
|
||||
'beacon.noticeTitle': 'Denne installation melder, at den findes',
|
||||
'beacon.noticeBody':
|
||||
'Én gang i døgnet sender Verdande to værdier til projektet, så det kan ses, hvor mange installationer der er. Præcis de her to — intet andet, ingen IP bliver gemt, intet om hvad du bruger det til:',
|
||||
'beacon.noticeTo': 'Sendes til {url}',
|
||||
'beacon.noticeKeep': 'Behold den',
|
||||
'beacon.noticeStop': 'Slå den fra',
|
||||
'beacon.noticeMore': 'Læs mere',
|
||||
'beacon.noticeKept': 'Tak. Du kan altid slå den fra under Indstillinger → Data.',
|
||||
'beacon.noticeStopped': 'Meldingen er slået fra. Der bliver ikke sendt noget.',
|
||||
'beacon.lastError': 'Sidste forsøg nåede ikke frem ({when}): {why}',
|
||||
'beacon.publishMin': 'Offentliggør først fra',
|
||||
'beacon.publishMinHint':
|
||||
'Under det tal svares der ingenting. Et lille tal fortæller mest om, hvor lidt der skal til at flytte det — og et, der falder, ville blive læst som “færre bruger det nu”, også når det var en udrulning, der gik galt.',
|
||||
'beacon.publish': 'Offentliggør tallet på /api/v1/beacon/count',
|
||||
'beacon.publishHint': 'Uden login, mellemlagret et minut. Viser installationer set de seneste 30 dage — ikke et løbende total, som kun ville stige.',
|
||||
'notes.newHere': 'Ny note',
|
||||
|
||||
@@ -87,6 +87,22 @@ export const en = {
|
||||
'beacon.active7': 'Active 7 days',
|
||||
'beacon.active30': 'Active 30 days',
|
||||
'beacon.byVersion': 'By version (30 days):',
|
||||
// The notice said unprompted the first time. The values are in it rather than
|
||||
// the word 'anonymous': a promise about telemetry is worth exactly as much as
|
||||
// the reader's ability to check it.
|
||||
'beacon.noticeTitle': 'This installation reports that it exists',
|
||||
'beacon.noticeBody':
|
||||
'Once a day Verdande sends two values to the project, so it can see how many installations there are. Exactly these two — nothing else, no IP is stored, nothing about what you use it for:',
|
||||
'beacon.noticeTo': 'Sent to {url}',
|
||||
'beacon.noticeKeep': 'Keep it on',
|
||||
'beacon.noticeStop': 'Turn it off',
|
||||
'beacon.noticeMore': 'Read more',
|
||||
'beacon.noticeKept': 'Thank you. You can always turn it off under Settings → Data.',
|
||||
'beacon.noticeStopped': 'Reporting is off. Nothing is being sent.',
|
||||
'beacon.lastError': 'The last attempt did not arrive ({when}): {why}',
|
||||
'beacon.publishMin': 'Publish only from',
|
||||
'beacon.publishMinHint':
|
||||
'Below that number nothing is answered. A small number mostly says how little it takes to move it — and one that falls would read as “fewer people use this now”, even when it was a release that went wrong.',
|
||||
'beacon.publish': 'Publish the count at /api/v1/beacon/count',
|
||||
'beacon.publishHint': 'No login, cached for a minute. Reports installations seen in the last 30 days — not a running total, which would only ever climb.',
|
||||
'notes.newHere': 'New note',
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import TaskDetail from '$lib/components/TaskDetail.svelte';
|
||||
import { t } from '$lib/i18n.svelte.js';
|
||||
import { setBadge } from '$lib/badge.js';
|
||||
import BeaconNotice from '$lib/components/BeaconNotice.svelte';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
@@ -208,6 +209,11 @@
|
||||
|
||||
<CommandPalette bind:open={paletteOpen} />
|
||||
|
||||
<!-- Sagt uopfordret, første gang en administrator er logget ind: hvad denne
|
||||
installation melder ind, og hvordan man siger nej. Komponenten tegner
|
||||
ingenting, når der ikke er noget at fortælle. -->
|
||||
<BeaconNotice />
|
||||
|
||||
<!-- Mounted here rather than in each view, so a task opens the same way from
|
||||
Today, a project, a label or a saved filter. -->
|
||||
{#if app.detailTask}
|
||||
|
||||
@@ -534,6 +534,18 @@
|
||||
<pre class="payload">{t('beacon.exactly')}
|
||||
{JSON.stringify({ instance_id: beacon.instance_id, version: beacon.version }, null, 2)}</pre>
|
||||
|
||||
{#if beacon.last_error}
|
||||
<!-- Et beacon, der ikke kan nå sin collector, fejler ellers i fuldstændig
|
||||
tavshed: det prøver igen i morgen, og "sidst sendt" bliver bare ved
|
||||
med at blive ældre. Ikke en fejl, der skal afbryde nogen — en linje,
|
||||
hvor siden ellers påstår, at alt er sendt. -->
|
||||
<p class="hint">
|
||||
{t('beacon.lastError', {
|
||||
when: new Date(beacon.last_error_at).toLocaleString(tag()),
|
||||
why: beacon.last_error
|
||||
})}
|
||||
</p>
|
||||
{/if}
|
||||
{#if beacon.last_ping_at}
|
||||
<p class="hint">{t('beacon.lastPing', { when: new Date(beacon.last_ping_at).toLocaleString(tag()) })}</p>
|
||||
{/if}
|
||||
@@ -592,6 +604,20 @@
|
||||
/>
|
||||
<span>{t('beacon.publish')}</span>
|
||||
</label>
|
||||
|
||||
{#if beacon.publish_count}
|
||||
<div class="field">
|
||||
<label for="publishmin">{t('beacon.publishMin')}</label>
|
||||
<input
|
||||
id="publishmin"
|
||||
type="number"
|
||||
min="0"
|
||||
value={beacon.publish_min}
|
||||
onchange={(e) => saveBeacon({ publish_min: Number(e.currentTarget.value) })}
|
||||
/>
|
||||
<p class="hint">{t('beacon.publishMinHint')}</p>
|
||||
</div>
|
||||
{/if}
|
||||
<p class="hint">{t('beacon.publishHint')}</p>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user