Yggdrasil is self-hosting too, so labelling only the Docker page
"Self-hosting" was misleading and put it above the Rune it was built for.
Reorder "As a Rune" above the Docker page, rename that page "With Docker",
and make the landing hero's primary call to action the Rune, with Docker
beside it.
Also drop the "the grammar is Todoist's" line from the filters page — the
factual import/export interoperability keeps naming Todoist, but the
positioning and soft comparisons do not.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claiming to be Todoist Pro is both inaccurate and an invitation to a
trademark complaint. Describe verdande on its own terms — a fast,
self-hosted task manager — and keep the Todoist references to what they
factually are: import/export interop, and a filter grammar close enough
that people coming from there already know it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The landing page and the docs now show and describe what has landed since
the last site update:
- the delegated-task marker (an arrow to whoever has it) — visible now in
the board, list and Waiting-on-others shots
- snooze — one seeded task is parked so the board shows a card greyed at
the foot of its column
- Web Push reminders to iPhone and Mac, dated tasks in a subscribed
calendar, task durations on the calendar, and per-person note sharing
Screenshots regenerated against the current UI (PNG for docs/README, WebP
for the landing page, from the one capture). "Lately" brought current.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The assignee chip reads as a delegation now, so its title changed from
"Ansvarlig: {name}" to "Uddelegeret til {name}". Point the role-edit
smoke test at the new title.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A task assigned to somebody who is not you is out of your hands and into
theirs, but the row only showed their name — the same weight as any
other meta. The assignee is now a bordered chip led by an outgoing arrow
in the accent colour, so a list reads at a glance as which tasks are
yours to do and which you are waiting on. Covers the board too: its
cards render a TaskRow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On a phone the detail drawer's field grid stayed two columns, so each
field got half the viewport. Prioritet collapsed to "Ing" and the due
row — a date input plus fixed-width time and duration fields — ran off
the right edge. Below 560px the grid now stacks to one column per field
and the due row wraps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Snooze puts a task out of the way without finishing it or filing it: it sinks to
the bottom of its list and greys, and comes back on its own when its time passes.
No job wakes it — "snoozed" is just snoozed_until being still in the future, read
against the clock each time the list is ordered.
- `snoozed_until` on tasks (migration 0031); the list order sinks a still-snoozed
task below everything, and it rejoins its place the moment the time passes.
- POST /tasks/{id}/snooze {until} — an RFC3339 time to snooze to, or empty to wake.
Nothing else about the task moves; a snooze is about when you want to see it, not
when it is due.
- The task drawer gains quick picks (this evening / tomorrow / next week) and a
custom time, and a Wake now once it is snoozed. The row dims and carries a
"snoozed until X" mark, and the list keeps snoozed tasks at the foot — the client
sorts the same way the server does, so a drag-sorted list does not jump on reload.
Tests: snooze sinks a task below one made after it and waking returns it (Go); and
from the drawer the row greys, shows the mark, and un-greys on wake (e2e). Full
suites green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The calendar feed emitted every task as a VTODO and, on top of that, an event only
for tasks that carried a clock time. But a subscribed calendar in Apple Calendar or
Google ignores VTODO entirely and draws only events — so a task with just a due
date, which is most of them, appeared in the verdande app's own calendar and
nowhere in the calendar it had been subscribed into. "Verdande does not update in my
calendar, only in the verdande app."
Every dated, unfinished task is an event now: a clock time makes a timed event as
before, a bare date an all-day one (a DATE start and an exclusive next-day end, so
it stays on its own day). Finished tasks stay off the calendar, as they were.
Test updated: the sample's timed task and its two date-only tasks all become events
now, the all-day ones in the DATE form, and the finished one still makes none.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every Web Push to an iPhone or a Mac was refused, silently as far as the person
waiting for the reminder could tell. The cause was the VAPID "sub" claim: on an
instance with no mail configured it fell back to mailto:verdande@example.invalid,
and Apple answers 403 Forbidden to a mailto whose domain is made up. Verified
against web.push.apple.com on the live subscriptions — that mailto gets a 403, the
instance's https URL a 201.
So the subject is now a real mailto when SMTP.From is a real address, and otherwise
the instance's own https base URL — which is a valid VAPID subject, points at
something that exists, and every push service including Apple accepts.
Two things found alongside it, both fixed:
- A successful push did not clear the failure count. A subscription drops at ten
failures, and all four live ones sat at nine from this bug — one flaky push from
being deleted despite working. MarkPushDelivered zeroes the count and moves
last_used_at on every delivery, so a subscription that recovers is not still
carrying its old failures toward the cliff.
- A reminder with no project made a notification whose URL was "/projekt/" — a
dead link. Empty project now points at the app root.
Test: the subject is a real mailto when there is one, and the https URL when there
is not — including for the localhost default, which is not a real address.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Archiving a single note meant finding its row in the list and its hover-only
button. With a note open to read or write, the one place you would reach for it —
its own footer, beside Gem and Slet — had no way to put it away.
There is one now: Arkivér between Save and Delete, which reads "bring it back" once
the note is already archived. Owner only, like delete, and it runs the same archive
the row button does — the note leaves the list without leaving the account.
e2e: archiving from the footer takes the note off the list, the archive holds it
with a bring-back button, and bringing it back returns it. The existing bulk-archive
test is scoped to the selection bar, now that "Arkivér" also lives in the footer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Choosing a note from the [[ completion wrote [[Title]] as plain text. It made a
real link behind the scenes — it showed in the panel under the note — but in the
note itself it stayed brackets, and "it doesn't become a link" is exactly how that
reads. A wiki link is a link where it is written.
Now a [[note]] renders inline as the title, in the accent colour, and a click on it
opens the note it points at. It is atomic — contenteditable="false" — so it moves
and deletes as one thing and a click is a click, not a caret landing mid-word. And
it appears the moment it is chosen: the completion drops the finished link in,
brackets and all swallowed, rather than text waiting to become a link when the note
is next opened.
The seam holds both ways: markdownToHtml lifts [[title]] into the link the same way
it already lifts an image, and htmlToMarkdown reads the link back to [[title]] by
the title it carries — so what is on disk is still the same Markdown, and a note
opened, saved and reopened is unchanged.
e2e: choosing a note writes an inline link, it survives the round-trip through
Markdown and a reload, and clicking it opens the note it names. Full suite green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Picking a note from the [[ completion wrote [[Title]] and the server made a real
link from it — but the link only ever showed in the panel under the note as plain
text. It said the note pointed somewhere and gave no way to go there, which reads
as "it didn't become a link at all".
The links in that panel are buttons now. A [[note]] resolves by its title to the
note — the one already on the list, or looked up if not — and opens it in place; a
#project goes to its page; a task link to the task. Titles with punctuation work,
brackets and parentheses included, because the match is on the whole title, not a
parse of it.
e2e: after linking a note, clicking its chip opens the note it names.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AllNotes returns newest-first by the second, so two notes written in the same
second have no order between them — and the notes export walks that list assigning
filenames, giving the first note of a name the plain filename and the next the
"(2)". With the order undecided, which of two same-titled notes got which name was
a coin toss that landed differently on each export, and the export test failed
roughly one run in three.
Sort the export by when each note was made, with the id to break a same-second
tie. The first note of a name keeps the name; the archive is the same twice
running. Ids are time-ordered, so the tie-break agrees with creation order rather
than fighting it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Typing [[ did nothing. The toolbar hint offered "[[ another note" and the parser
turned a fully-typed [[Title]] into a link on save, but the completion that makes
it usable — the one # has for projects — was never there, so you had to know and
type a note's title exactly, brackets and all.
Now [[ opens the same picker # does, over the notes the page has loaded: the
current note is left out (a note linking to itself is a loop nobody meant), and
one without a title yet cannot be pointed at. Choosing a note closes its own
brackets — the opener is already in the text — and leaves a space, so the next
word is a word and not more of the tag. On save the link is real and shows in the
note's links, not merely as text.
The picker's two kinds are told apart where a project wears its colour: a note
suggestion wears the brackets that open it.
e2e: [[ offers a note by title, choosing it writes [[Title]] into the note, and
the link lands in the note's links. Full Chromium suite green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The landing page did not mention two things the app grew — sharing a note with a
person, and task durations — and its screenshots were all in the dark theme.
- Copy: a cell for sharing a note or a page (to a person, or by filing it in a
project, with the Shared with me group), and one for how long a task takes (drawn
to length on the calendar, dragged by its foot, kept when moved).
- Two new screenshots in the light theme, at the top of the gallery: the calendar
drawing timed tasks to their real length, and a note somebody shared gathered
under Shared with me. English throughout, like the rest of the site — the capture
account is English, so the interface is.
The generator grew the demo data these need: a handful of timed tasks with lengths
across the week, and a note the colleague shares back to the owner. Every other
screenshot was recaptured from the current build in the same pass, so the whole set
reflects what ships today (the tidied note footer, and the rest).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A timed task was drawn at a fixed half hour whatever its `duration_min` said, so a
length the data model already carried was invisible and uneditable. Three parts:
- The calendar draws a task at its real length — a quarter-hour floor so a
zero-minute task is still grabbable, the day's end as the ceiling — instead of a
flat thirty minutes. A ninety-minute task now looks like ninety minutes.
- A grip along the foot of a timed task drags its length. Pointer events, not the
HTML5 drag the body uses, and stopPropagation between them — otherwise making a
task longer would pick it up and move it instead. It snaps to the quarter hour,
follows the handle live, and writes once on release through a new store `resize`.
Moving a task (the existing day/time drag) leaves the length alone.
- The task drawer gains a length field beside the time — minutes, shown only with
a time to hang it on, since a duration without a start is a length of nothing and
the calendar has nowhere to draw it. So a length can be set off the calendar too.
e2e: dragging the foot of a task makes it longer both on screen and in what the
server stores, and the time does not move. Full Chromium suite green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sharing controls were laid out across the note's footer — the project select,
the whole "Del med personer" panel and the Save/Delete buttons all on one row. It
crammed: "Læg i projekt" wrapped to two lines and the people panel ran into the
rest.
Now one Del button in the footer opens a popover that holds both ways a note is
shared — a project and named people — stacked in a small card. The footer is a
status on the left and Del / Gem / Slet on the right, and nothing wraps. The
button carries a count when the note is shared with someone, so "shared" is
legible without opening it.
The popover closes on a click outside, on Escape, and on switching to another
note — the last tracked by note id, so a save returning a fresh copy of the same
note does not slam it shut mid-use. The inner "add someone" button is now Tilføj,
so it does not read as a second Del.
e2e updated to open the popover before reaching the controls; full Chromium suite
green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A <pre> is preformatted, so the browser answered Enter by inserting a <br> and
keeping the caret inside it. A monospace last line therefore trapped everything
typed after it in monospace, with no way back to body by typing — and the style
menu could not get out either: `formatBlock('p')` on a line inside a <pre> nests a
<p> *inside* the block (`<pre>…<p>body</p></pre>`), so the text stayed monospace
and the note was left malformed. Reported as "monospace keeps going and I can
neither delete it nor switch to body".
Two fixes, both in the editor:
- Plain Enter in a <pre> now steps out: the tail after the caret becomes a
paragraph below the block, and that is where the caret lands. Shift+Enter is
untouched, so a soft line break still writes multi-line code inside the block —
Enter to leave, Shift+Enter to stay.
- The style menu converts anything touching a <pre> by hand, replacing the element
rather than reformatting inside it, so a monospace line goes cleanly back to body
(or to a heading) instead of nesting.
The split can leave an empty text node in the new paragraph, and a block with no
element and no text is not a caret host in Chromium — the next keystroke falls back
into the block above. So an empty paragraph is given a <br> to stand in, which was
the difference between the caret landing in the new line and the text reappearing
in the code.
e2e: a monospace line left on Enter is body after it, Shift+Enter stays monospace,
the menu returns it to body, and the whole thing survives the round-trip through
Markdown and a reload.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The calendar test asserted `[data-date=day] .tevent` had a count of exactly one —
the mocked meeting — after dragging its task to the next day. That count assumed
the day held nothing but this test's own events. It does not: the suite shares one
database, and an earlier test drags a task across a month boundary, which on some
dates lands a timed task on this very day. A timed task renders as a `.tevent`
too, so the count was two, and the test failed — but only in the full run, and
only when the date arithmetic put the two tests on the same cell. Alone it passed,
which is exactly the shape of a shared-state collision rather than a real bug.
Named, not counted: assert the meeting is still there and the task is no longer in
the day's column, which is what "the events stayed, the task moved" actually
means — and is true however full another test left the day. Same for the all-day
band. No product code changed; the drag and everything it checks are untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The project-filing control was relabelled "Del i" → "Læg i projekt" and its empty
option "Kun mig" → "Intet projekt" (a note can now be shared with people while
under no project, so "kun mig" was no longer true). The two e2e tests that drove
that control by its old label are updated to the new one.
And a new test for the feature itself: an owner shares a note with a second
account from the Del med personer panel, the recipient's own list grows a Delt
med mig group with the note in it, a viewer's row carries no star or archive
control, and unsharing takes it back off their list. The recipient is navigated
on the origin the invite signed her in on — VERDANDE_BASE_URL is localhost while
the suite drives 127.0.0.1, and a session cookie set on one host is not sent to
the other.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Until now a note was shared exactly one way: file it in a shared project, and
everyone who could read the project could read the note. That is right when the
note belongs with a body of work and wrong when it is just "let Sofie see this" —
it made you stand up a project to hand over a single page.
A note can now be shared with named people directly, at viewer or editor — the
same two roles a project membership uses, so there is one idea of a role in the
program and not two. The paths stack: a note can be loose, filed, shared with
people, or any mix, and access is the most any of them allow.
The store:
- note_shares (migration 0030): note ↔ user ↔ role, one row per person, owner
recorded. Visibility (AllNotes, and search via mayTouchNote) widens to include
notes shared directly with the reader.
- the effective role folds a direct share on top of the owner and project paths.
The rules, drawn where they protect the owner:
- read = viewer, edit text = editor, on any path.
- MOVE a note (change its project) is owner-only: an editor filing it into a
project the owner is not in would take the owner's own note away from them.
- DELETE is owner or project-editor — a direct share, however high its role,
does not reach to throwing the note away.
- SHARE and UNSHARE are the owner's alone, like a project's invitations.
- the picker is everyone on the instance (a closed, invited set) minus who is
already on the note; a made-up id and the owner themselves are refused.
The list:
- a "Delt med mig" group at the top, collapsible, that appears only when it has
something in it — the same rule Favoritter follows — so it costs nothing when
you share nothing. A shared note lives there, not also in a time bucket.
- every shared note carries a small chip in its owner's avatar colour, the
project mark's shape said for a person.
- no star or archive button on a note somebody else owns: those change the note
itself, and it is not yours to put away.
- the editor grows a "Del med personer" panel for the owner: who can see it, at
what role, and an add row drawn from the instance's other people.
The old comment in the notes footer argued against a note having an access list
of its own. That was the right call until the case it did not cover — sharing
without a project — turned out to be the one actually asked for. Updated to match.
Person now carries json tags, so a note's owner serialises as id/name/avatar_color
like every other person in the API rather than as Go field names.
Tests: a shared note reaches the other person marked and owned; a viewer reads but
cannot write; an editor writes but cannot re-share or delete; unshare removes
access and is owner-only; the candidate list excludes who is already on and the
owner; and a made-up id and self are refused. OpenAPI covers the three new routes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RFC 6238 says the verifier must reject a code it has already accepted: a code
stays valid for its whole thirty-second window, so one seen in flight — a
phishing page, a proxy, over the shoulder — could be replayed until it expired.
Tasks now record the counter of the last accepted step (totp_last_step), and a
login accepts a code only when its step is strictly greater. The guard is in the
UPDATE, not a read-then-write, so two logins racing with the same stolen code
cannot both find the old value and both proceed — exactly one moves the row.
auth.TOTPStep is VerifyTOTP that also reports which step matched, walking the
same one-step window; the code entered a little early or late spends the step it
was issued at, not the step it was checked at, so a late-but-valid code is still
single-use. Verified end to end: the same code presented to two logins in one
window is accepted once and refused the second time.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The rate limiter and the audit log key on the caller's address, and that address
was read with chi's middleware.RealIP — which believes X-Forwarded-For from
anyone. So a caller could set it to a new value on every request and get a fresh
password-guess bucket each time, and could write any address they liked into the
log the panel's watchers read. The one control meant for when a password is
already gone was keyed on a number the attacker got to pick.
Now the header is believed only when the machine that *connected* is a trusted
proxy — by default anything on a private network, which is the shape of a reverse
proxy or tunnel sharing the host or the docker bridge. A connection straight off
the internet is trusted for nothing and is keyed on the address it actually came
from. VERDANDE_TRUSTED_PROXIES overrides the set (CIDRs, or "none" to trust no
proxy at all); VERDANDE_REAL_IP_HEADER names the header.
Verified in the current deployment before the change: behind the Cloudflare
Tunnel a spoofed X-Real-IP already got no fresh bucket, because cloudflared
overwrites it. This makes that safety a property of the code, not of the
deployment — expose the instance directly, or put a header-passing proxy in
front, and it would have been a live hole.
Also COOP: Cross-Origin-Opener-Policy: same-origin on every response, isolating
the browsing context. verdande scripts no other window, so there is nothing to
lose and a class of cross-window attacks to close.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
To headere, appen ikke satte, og som begge følger af, hvad den allerede er.
Sessionen bor i en __Host-cookie, der kun findes over HTTPS — hele modellen
forudsætter TLS. Uden HSTS holder den forudsætning ikke mod en netværksangriber:
den allerførste navigation kan strippes til http://, og cookien rækkes over i
klartekst, før Secure-flaget nogensinde kommer i spil. HSTS lukker vinduet, og
den er sat på nøjagtig samme betingelse som cookiens Secure-flag (secureCookies),
så en dev-instans på ren HTTP aldrig bliver bedt om et certifikat, den ikke har.
To år, underdomæner med; ingen preload — at blive hard-kodet ind i browsere er en
beslutning for domænets ejer, ikke for en selv-hostet app.
verdande beder aldrig om kamera, mikrofon eller placering, så den sikreste
Permissions-Policy er at slå dem alle fra: et indsat element kan ikke bede om det,
siden selv har afvist. Navngivet felt for felt frem for med et jokertegn, fordi
jokertegns-formen ikke forstås ens af alle browsere, og en regel, en browser
ignorerer, er ingen regel.
Prøve på begge: HSTS følger HTTPS og udebliver over HTTP, og Permissions-Policy
lukker for kameraet uanset skema.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fundamentet under Kvasir, og det er værd at have uden ham.
Intet kunne svare på "er denne mail allerede blevet til en opgave?". To læsere
af samme postkasse — en anden assistent, en gentaget kørsel, et menneske —
laver derfor to opgaver. Det er ikke teoretisk: en briefing lavede "Betal
faktura fra Browns" to gange i samme kørsel.
Opgaver har nu en `source_key`, navngivet med et rum foran: `gmail:thread:…`,
`imap:<vært>:<mappe>:<uid>`. Et Gmail-tråd-id og et IMAP-uid ligner ikke
hinanden, lige indtil den dag de kolliderer, og et kolon koster ingenting. Det
betyder også, at den næste kilde ikke skal bruge en ny kolonne — pointen med en
nøgle frem for en fremmednøgle. Der findes ingen messages-tabel at pege på, og
det her skal virke uden.
Et delvist unikt indeks er selve låsen, ikke en optimering: to agenter, der
læser samme indbakke, SKAL være ude af stand til at lave to opgaver, og "skal
være ude af stand til" er en betingelse frem for en aftale, nogen husker.
Afgrænset til den, mailen kom til — to personer på samme instans, der begge
modtager tråden, får hver sin opgave, og det er rigtigt.
CreateTask svarer ErrDuplicate frem for at oprette nummer to, og begge
mail-veje springer over i stedet for at fejle. IMAP-vejen tæller den stadig som
læst — ellers rykker last_uid sig aldrig forbi den, og hver eneste kørsel fra da
af standser samme sted.
`TaskIDBySource` er spørgsmålet den anden vej, så en assistent, der ikke ejer
triagen, kan se, at mailen er håndteret. Slettede opgaver tæller med: en opgave,
der blev lavet af en mail og smidt væk, er også et svar.
Grøn i månedsvis, rød i morges — på en commit, der ikke var i nærheden af en
kalender. Det kostede en runde at tro, det var min egen ændring.
Testen lægger opgaven på i morgen og trækker den til dagen efter. Er i morgen en
søndag, er kilden sidste kolonne i sin række og målet første celle i den næste —
nede i den del af gitteret, et 720 px vindue skærer af. Et syntetisk træk slipper
i målets midtpunkt uden den auto-scroll, en rigtig markør får af browseren, så et
mål under folden bliver aldrig ramt. I går lå begge celler i samme række.
To ting rettet, og den anden er den vigtigste:
- Vinduet er højt nok til alle seks rækker under månedsdelen.
- Målet tages fra gitteret frem for at blive regnet ud af datoen. Dagen efter i
morgen ligger ikke altid i den måned, der vises — i enden af en kort måned
falder den udenfor — mens cellen ved siden af altid gør. Med et fald tilbage
til cellen før, for den ene dag om måneden hvor der ikke er en efter.
Appen fejler ikke noget: en rigtig bruger ruller.
Adressen rettet fem steder tilbage til ghcr.io. Og rune-siden lærte folk at
teste synlighed med `docker --config /tmp/empty pull`, hvilket er den fejl,
der tog instansen ned: en tom config stopper ikke maskinens credential-helper,
så pullet autentificeres uden at se sådan ud og melder succes for et image,
ingen fremmed kan hente. Prøven er nu registrets token-endepunkt med curl.
v5 flyttede imaget til gitea.nolimit.dk og tog instansen ned. Panelets
PullImage sætter aldrig RegistryAuth, så registret skal kunne læses uden
legitimation, og det kan Gitea her ikke: token-endepunktet svarer 401 for
kw/verdande, også efter at repoet blev gjort offentligt. Instansen kræver login
på alt, registret inklusive.
Jeg påstod det modsatte, og beviste det med en test, der ikke kunne bevise
noget: `docker --config <tom-mappe> pull`. En tom config stopper ikke maskinens
credential-helper, så pullet blev autentificeret uden at se sådan ud. Den
rigtige prøve er registrets token-endepunkt med curl — GHCR svarer 200 med et
token, Gitea svarer 401. Begge dele målt.
Fejlen er værd at kunne genkende: panelet river containeren ned for at anvende
runen, pullet fejler, pullImageRetry kasserer fejlen, og ContainerCreate slår
derefter op lokalt og siger `No such image`. Serveren står som "running" uden
container_id, udadtil en 502, og intet sted nævnes det manglende login.
Imaget ligger nu begge steder — samme digest, sha256:f7b23efd… — så koden bliver
i Gitea, mens containeren hentes fra GHCR, indtil Gitea kan servere anonymt.
Jeg skrev instansens faktiske Tailscale-adresse ind som prøvedata. Den er
ubrugelig uden adgang til tailnettet, men den er stadig et kort over én persons
opsætning — og af præcis den grund er arbejdspapiret om flytningen gitignoreret.
Så skal en test ikke bære adressen i stedet. En neutral adresse fra samme
område prøver det samme.
Rapporteret som "AI gemmer ikke mine oplysninger". Anden gang det symptom
meldes, og en helt anden årsag end sidst — dengang delte scopes i user_settings
én række og slettede hinanden.
Udbyderen hedder "OpenAI-kompatibel (Ollama, LM Studio, …)" og feltet foreslår
selv http://localhost:11434/v1. safedial nægter præcis den slags adresser. Så
den ene udbyder, der findes for at køre sin egen model, kunne ikke sættes op,
og formularen foreslog en adresse, serveren ville afvise.
Serveren svarede 422 med en fejl på base_url. Formularen havde ingen plads til
at vise en fejl på det felt, og fordi fejlobjektet ikke var tomt, blev der
heller ikke vist en toast. Beskeden forsvandt, og det så ud, som om der ikke
skete noget ved at trykke Gem.
Tre rettelser:
- Private adresser tillades for admins. Muren står, fordi en server, der henter
en brugerangivet adresse, lader den, der angiver den, nå ting han ellers ikke
kan. En admin på en selvhostet instans ejer allerede værten og dens netværk,
så det giver ham intet nyt. For en inviteret bruger er den lukket som før.
Tilladelsen regnes ud pr. forespørgsel, ikke gemt ved siden af adressen — den,
der ikke længere er admin, skal ikke kunne nå indenfor på et gammelt flag.
- Tailnet er ikke "indenfor". Halvdelen virkede allerede ved et tilfælde: Gos
IsPrivate dækker RFC1918 og ikke det CGNAT-område, Tailscale låner, så 100.x
slap igennem uden at nogen havde besluttet det. IPv6-halvdelen gjorde ikke —
fd7a:115c:a1e0::/48 ligger inde i fc00::/7. Samme maskine var altså
tilgængelig eller ej alt efter adressefamilie. Begge områder er nu navngivet,
hvilket gør IPv4-halvdelen bevidst frem for heldig.
- Feltfejl kan ikke længere forsvinde. base_url og model har fået et sted at
vise en, og en fejl på et felt, der ikke har et, råbes op som en toast frem
for at blive tabt.