Merge pull request #66 from kristianwind/indbakken-har-en-adresse-apps-kan-skubbe-til
CI / OpenAPI is valid (push) Successful in 11s
CI / End-to-end (push) Successful in 11m3s
CI / Docker image builds (push) Successful in 2m17s
Release / Build and publish image (push) Failing after 9s
CI / Go (push) Failing after 15m6s

feat(indbakke): en adresse, andre programmer kan skubbe en opgave til
This commit is contained in:
Kristian Wind
2026-09-08 14:12:17 +02:00
committed by GitHub
10 changed files with 633 additions and 0 deletions
+5
View File
@@ -418,6 +418,11 @@ export const api = {
getMailAddress: () => get('/mail-address'),
rotateMailAddress: () => post('/mail-address/rotate'),
// Krogen ind i indbakken. Selve skubbet sker ikke herfra — det er et andet
// program, der gør det — så der er kun adressen at hente og at skifte.
getHookURL: () => get('/hook-url'),
rotateHookURL: () => post('/hook-url/rotate'),
getGmail: () => get('/gmail'),
setGmail: (data) => put('/gmail', data),
disconnectGmail: () => del('/gmail'),
+11
View File
@@ -751,6 +751,17 @@ export const da = {
'Der er ingen mailserver sat op på denne server, så adressen herunder kan ikke modtage noget endnu. Sæt VERDANDE_SMTP_HOST og de øvrige SMTP-indstillinger, og få mailserveren til at aflevere post til todo+* videre til /inbound/mail.',
'int.yourAddress': 'Din adresse',
'int.newAddress': 'Ny adresse',
// Krogen ind i indbakken: en URL, et andet program kan skubbe en opgave til.
'int.hookToTask': 'Skub fra andre programmer',
'int.hookHint':
'En adresse, andre programmer kan sende en opgave til — en genvej på telefonen, et script, en tjeneste, der kan kalde en URL. Linjen læses som i feltet øverst i fladen, så “i morgen”, “p1” og “#Projekt” betyder det samme her.',
'int.yourHook': 'Din adresse',
'int.hookHow':
'Send en POST med teksten som krop — ren tekst, JSON med et {text}-felt, eller et formularfelt. Første linje bliver opgaven, resten står under den. Adressen er en nøgle: den, der har den, kan lægge opgaver i din indbakke og ikke andet.',
'int.hookExample': 'Sådan ser det ud',
'int.newHook': 'Ny adresse',
'int.newHookQuestion':
'Vil du have en ny adresse? Den gamle holder op med at virke med det samme, og alt, der bruger den, skal rettes.',
'int.caldav': 'CalDAV',
'int.caldavHint':
'Tovejs, i modsætning til feedet ovenfor: Apple Påmindelser og Thunderbird kan også skrive tilbage.',
+11
View File
@@ -748,6 +748,17 @@ export const en = {
'No mail server is configured on this server, so the address below cannot receive anything yet. Set VERDANDE_SMTP_HOST and the other SMTP settings, and have the mail server deliver post for todo+* to /inbound/mail.',
'int.yourAddress': 'Your address',
'int.newAddress': 'New address',
// The hook into the inbox: a URL another program can push a task to.
'int.hookToTask': 'Push from other programs',
'int.hookHint':
'An address other programs can send a task to — a shortcut on your phone, a script, any service that can call a URL. The line is read the way the box at the top of the app reads it, so “tomorrow”, “p1” and “#Project” mean the same here.',
'int.yourHook': 'Your address',
'int.hookHow':
'Send a POST with the text as the body — plain text, JSON with a {text} field, or a form field. The first line becomes the task and the rest sits underneath it. The address is a key: whoever has it can put tasks in your inbox, and nothing else.',
'int.hookExample': 'What it looks like',
'int.newHook': 'New address',
'int.newHookQuestion':
'Do you want a new address? The old one stops working immediately, and anything using it has to be changed.',
'int.caldav': 'CalDAV',
'int.caldavHint':
'Two-way, unlike the feed above: Apple Reminders and Thunderbird can write back as well.',
@@ -289,6 +289,9 @@
let feedURL = $state('');
let mailAddress = $state('');
// Krogen ind i indbakken: den URL, en genvej på telefonen eller et script kan
// skubbe en linje tekst til.
let hookURL = $state('');
/** Null until loaded, so the warning does not flash before the answer is in. */
let mailConfigured = $state(null);
@@ -301,6 +304,10 @@
mailConfigured = r.configured;
})
.catch(() => {});
api
.getHookURL()
.then((r) => (hookURL = r.url))
.catch(() => {});
});
async function rotateFeed() {
@@ -323,6 +330,15 @@
}
}
async function rotateHook() {
if (!confirm(t('int.newHookQuestion'))) return;
try {
hookURL = (await api.rotateHookURL()).url;
} catch (e) {
app.toast(humanMessage(e));
}
}
async function copy(text) {
try {
await navigator.clipboard.writeText(text);
@@ -674,6 +690,38 @@
</div>
</section>
<section class="panel">
<header>
<h2>{t('int.hookToTask')}</h2>
<p class="hint">
{t('int.hookHint')}
</p>
</header>
<div class="field">
<label for="hook">{t('int.yourHook')}</label>
<input id="hook" class="mono" value={hookURL} readonly />
<p class="hint">
{t('int.hookHow')}
</p>
</div>
<!-- Vist som den kommando, den er. Den, der skal sætte det op, skal kunne
prøve det af ét sted, før de bygger genvejen på telefonen — og se, at
kroppen bare er den linje, opgaven skal hedde. -->
<div class="field">
<label for="hookcurl">{t('int.hookExample')}</label>
<textarea id="hookcurl" class="mono" rows="3" readonly
>{`curl -X POST ${hookURL} \\\n -H "Content-Type: text/plain" \\\n -d "Ring til Anders i morgen p1 #Firma"`}</textarea
>
</div>
<div class="row">
<button class="secondary" onclick={() => copy(hookURL)}>{t('int.copy')}</button>
<button class="danger" onclick={rotateHook}>{t('int.newHook')}</button>
</div>
</section>
<section class="panel">
<header>
<h2>{t('int.caldav')}</h2>