feat(site): landingssiden siger hvad der findes nu, og siger det med kanter
Release / Build and publish image (push) Failing after 10m0s
Release / Build and publish image (push) Failing after 10m0s
Siden stod fra før kalenderen, noternes billeder og arkiv, sikkerhedsarbejdet og beaconet. Nu står de der: søgning der læser dansk, kalenderen, Venter på andre, gentagelser, AI slået fra som udgangspunkt, andenfaktor, at ingen skriver sig selv op, og beaconet — sagt som det er, tændt fra start og med ét klik til at slukke. Google Kalender står med sit forbehold på selve kortet. Den er skrevet efter dokumentationen og har aldrig kørt mod Google, og en landingsside, der siger noget andet, er den ene sætning, resten af siden ikke kan bære. Ikke-målene løj om posten: "No mail integration beyond Gmail and the forwarding address" har været forkert, siden IMAP kom til. iCloud og Fastmail virker. Det, der faktisk ikke virker, er Outlook, og det er nu det, der står. Designet: en bjælke der bliver stående, fordi siden er 5.600 px og vejen ud kun lå i toppen. Fire fakta under knapperne, hvor en landingsside plejer at sætte logoer, den ikke har gjort sig fortjent til. Kort med egne kanter frem for celler i ét gitter — ti celler i tre søjler er et hul i tegningen, atten kort i tre grupper er ikke. To paneler under "Running it", fordi de er alternativer og ikke trin. Ikke-målene som en liste, der kan tælles. Og tre skærmbilleder, der lå i site/screenshots uden at være brugt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8ecefbbdc2
commit
43f8c0ea85
+465
-81
@@ -6,12 +6,12 @@
|
|||||||
<title>verdande — self-hosted tasks, shared</title>
|
<title>verdande — self-hosted tasks, shared</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Self-hosted tasks and projects, shared. One Go binary with SQLite inside it. Your mail as an inbox, CalDAV, passkeys, and an MCP connector for Claude."
|
content="Self-hosted tasks, notes and projects, shared. One Go binary with SQLite inside it. Your mail as an inbox, a CalDAV server, notes in Markdown, passkeys, and an MCP connector for Claude."
|
||||||
/>
|
/>
|
||||||
<meta name="theme-color" content="#111312" />
|
<meta name="theme-color" content="#111312" />
|
||||||
|
|
||||||
<meta property="og:title" content="verdande" />
|
<meta property="og:title" content="verdande" />
|
||||||
<meta property="og:description" content="Self-hosted tasks and projects, shared. One binary, SQLite inside it." />
|
<meta property="og:description" content="Self-hosted tasks, notes and projects, shared. One binary, SQLite inside it." />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:image" content="screenshots/today.webp" />
|
<meta property="og:image" content="screenshots/today.webp" />
|
||||||
<meta property="og:image:alt" content="The Today view: projects and filters beside a list of what is due." />
|
<meta property="og:image:alt" content="The Today view: projects and filters beside a list of what is due." />
|
||||||
@@ -76,10 +76,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.theme-toggle {
|
.theme-toggle {
|
||||||
position: fixed;
|
/* In the bar now rather than floating over the corner. Fixed, it sat
|
||||||
top: 1rem;
|
on top of whatever happened to scroll under it — on a narrow window
|
||||||
right: 1rem;
|
that was the first screenshot's own corner. */
|
||||||
z-index: 10;
|
flex: none;
|
||||||
width: 2.25rem;
|
width: 2.25rem;
|
||||||
height: 2.25rem;
|
height: 2.25rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -141,10 +141,86 @@
|
|||||||
padding: 0 1.5rem;
|
padding: 0 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- the bar that stays -------------------------------------------
|
||||||
|
|
||||||
|
A page this long had its only way out at the very top: somebody who
|
||||||
|
read as far as "What it will not do" had to scroll back past nine
|
||||||
|
screenshots to reach the documentation. The bar is the way out from
|
||||||
|
wherever you are.
|
||||||
|
|
||||||
|
`sticky` and not `fixed`: sticky keeps its space in the flow, so
|
||||||
|
nothing below has to be pushed down by a number that then has to be
|
||||||
|
kept in step with the bar's own height.
|
||||||
|
|
||||||
|
Full width, with the wrap *inside* it. Put the other way round, the
|
||||||
|
background would end where the column ends and the page would scroll
|
||||||
|
past it on both sides. */
|
||||||
|
.topbar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 20;
|
||||||
|
background: var(--ground);
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.25rem;
|
||||||
|
padding: 0.625rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: var(--ink);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 560;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-brand .rune {
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 1.125rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-links {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.25rem;
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-links a {
|
||||||
|
color: var(--ink-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-links a:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The three in-page links go first on a narrow screen: they are the ones
|
||||||
|
a phone can reach by scrolling anyway. Documentation and Source cannot
|
||||||
|
be reached any other way, so they stay. */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.bar-links .inpage {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Or the bar covers the heading you just jumped to. */
|
||||||
|
:target {
|
||||||
|
scroll-margin-top: 4.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- hero --- */
|
/* --- hero --- */
|
||||||
|
|
||||||
header {
|
.hero {
|
||||||
padding: 6rem 0 4rem;
|
padding: 5rem 0 3.5rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +248,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.norn {
|
.norn {
|
||||||
margin: 2rem auto 0;
|
margin: 0 auto 1.75rem;
|
||||||
max-width: 34rem;
|
max-width: 34rem;
|
||||||
font-size: 0.9375rem;
|
font-size: 0.9375rem;
|
||||||
color: var(--ink-faint);
|
color: var(--ink-faint);
|
||||||
@@ -289,44 +365,104 @@
|
|||||||
|
|
||||||
/* --- features --- */
|
/* --- features --- */
|
||||||
|
|
||||||
|
/* A hairline between sections rather than under every heading: the page
|
||||||
|
is one column, and the line says where one subject stops. */
|
||||||
section {
|
section {
|
||||||
padding: 3rem 0;
|
padding: 3.5rem 0;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.75rem;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 560;
|
font-weight: 560;
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
margin: 0 0 2rem;
|
margin: 0 0 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A short accent rule in front of every h2. The sections had nothing to
|
||||||
|
mark them but their own words, and at a glance the page read as one
|
||||||
|
long run of paragraphs. */
|
||||||
|
h2::before {
|
||||||
|
content: '';
|
||||||
|
flex: none;
|
||||||
|
width: 1.125rem;
|
||||||
|
height: 2px;
|
||||||
|
border-radius: 1px;
|
||||||
|
background: var(--accent);
|
||||||
|
transform: translateY(-0.35em);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Three groups rather than eighteen cards in a row. Grouped, the list is
|
||||||
|
read as three answers — what it does, what it talks to, what it
|
||||||
|
promises — and any of the three can be skipped. */
|
||||||
|
.group-label {
|
||||||
|
margin: 2.5rem 0 1rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 560;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-label:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards with their own edges, not cells in one table.
|
||||||
|
The table shared a hairline grid, which meant the last row had to be
|
||||||
|
full or it was a hole in the drawing — and it was a hole, for as long
|
||||||
|
as there were ten of them. A card that stands alone on its row is a
|
||||||
|
card; an empty cell is a mistake. */
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
|
||||||
gap: 1px;
|
gap: 0.875rem;
|
||||||
background: var(--line);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell {
|
.cell {
|
||||||
background: var(--ground);
|
background: var(--surface);
|
||||||
padding: 1.5rem;
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 1.125rem 1.25rem 1.25rem;
|
||||||
|
transition: border-color 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell:hover {
|
||||||
|
border-color: var(--line-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell h3 {
|
.cell h3 {
|
||||||
margin: 0 0 0.5rem;
|
margin: 0 0 0.4375rem;
|
||||||
font-size: 1rem;
|
font-size: 0.9375rem;
|
||||||
font-weight: 560;
|
font-weight: 560;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell p {
|
.cell p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.9375rem;
|
font-size: 0.875rem;
|
||||||
color: var(--ink-muted);
|
color: var(--ink-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cell code {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.8125em;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A card that has something to admit rather than something to offer.
|
||||||
|
It is not a footnote and it is not fine print: it is the same size as
|
||||||
|
everything else, and it says the same thing the README says. */
|
||||||
|
.cell .caveat {
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
}
|
||||||
|
|
||||||
/* --- install --- */
|
/* --- install --- */
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
@@ -374,6 +510,109 @@
|
|||||||
color: var(--ink-muted);
|
color: var(--ink-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Four facts under the buttons, in the place where a landing page usually
|
||||||
|
puts logos it has not earned. They are the four questions somebody
|
||||||
|
self-hosting asks before anything else. */
|
||||||
|
ul.facts {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem 1.25rem;
|
||||||
|
margin: 2rem 0 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.facts li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The separator belongs between two facts, not after the last one — so it
|
||||||
|
is drawn by the item that follows it. */
|
||||||
|
ul.facts li + li::before {
|
||||||
|
content: '';
|
||||||
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--line-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On a phone the row wraps, and a separator drawn by the item that follows
|
||||||
|
it lands at the *start* of the new line — a dot with nothing before it.
|
||||||
|
No separators there; the wrap is the separation. */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
ul.facts {
|
||||||
|
gap: 0.375rem 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.facts li + li::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Two ways to run it, side by side, because they are alternatives rather
|
||||||
|
than steps. Stacked, the second read as what you do after the first. */
|
||||||
|
.panels {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
|
||||||
|
gap: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel h3 {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-weight: 560;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sunk rather than raised: inside a card, a block in --surface would be
|
||||||
|
the same colour as the card it sits in. */
|
||||||
|
.panel pre {
|
||||||
|
background: var(--ground);
|
||||||
|
margin: 1rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The non-goals were one paragraph of seven sentences, which is the shape
|
||||||
|
nobody reads. Each is its own line now, and they can be counted. */
|
||||||
|
ul.nogo {
|
||||||
|
list-style: none;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
|
||||||
|
gap: 0.5rem 1.75rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.nogo li {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.nogo li::before {
|
||||||
|
content: '—';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
}
|
||||||
|
|
||||||
.demo-note {
|
.demo-note {
|
||||||
margin: 1rem 0 0;
|
margin: 1rem 0 0;
|
||||||
font-family: var(--font);
|
font-family: var(--font);
|
||||||
@@ -524,27 +763,38 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<button class="theme-toggle" type="button" aria-label="Switch between light and dark" title="Light or dark">
|
<div class="topbar">
|
||||||
<span aria-hidden="true">◐</span>
|
<div class="wrap bar">
|
||||||
</button>
|
<a class="bar-brand" href="#top">
|
||||||
<div class="wrap">
|
<span class="rune" aria-hidden="true">ᚹ</span>
|
||||||
<header>
|
verdande
|
||||||
|
</a>
|
||||||
|
<nav class="bar-links" aria-label="Sections">
|
||||||
|
<a class="inpage" href="#screens">Screens</a>
|
||||||
|
<a class="inpage" href="#features">Features</a>
|
||||||
|
<a class="inpage" href="#running">Running it</a>
|
||||||
|
<a href="https://kristianwind.github.io/verdande/">Documentation</a>
|
||||||
|
<a href="https://github.com/kristianwind/verdande">Source</a>
|
||||||
|
<button class="theme-toggle" type="button" aria-label="Switch between light and dark" title="Light or dark">
|
||||||
|
<span aria-hidden="true">◐</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wrap" id="top">
|
||||||
|
<header class="hero">
|
||||||
<span class="mark" aria-hidden="true">ᚹ</span>
|
<span class="mark" aria-hidden="true">ᚹ</span>
|
||||||
<h1>verdande</h1>
|
<h1>verdande</h1>
|
||||||
<p class="lede">
|
<p class="lede">
|
||||||
Self-hosted tasks and projects, shared with the people you share the work with.
|
Self-hosted tasks, notes and projects, shared with the people you share the work
|
||||||
One binary, SQLite inside it.
|
with. One binary, SQLite inside it.
|
||||||
</p>
|
</p>
|
||||||
<p class="rune-note">
|
<p class="rune-note">
|
||||||
Built to be deployed as a <strong>Rune</strong> in
|
Built to be deployed as a <strong>Rune</strong> in
|
||||||
<a href="https://github.com/kristianwind/yggdrasil">Yggdrasil Panel</a> — upload
|
<a href="https://github.com/kristianwind/yggdrasil">Yggdrasil Panel</a> — upload
|
||||||
the manifest, fill in two fields, press start. It also runs anywhere Docker does.
|
the manifest, fill in two fields, press start. It also runs anywhere Docker does.
|
||||||
</p>
|
</p>
|
||||||
<p class="norn">
|
|
||||||
Verdande is one of the three Norns — the one who spins <em>that which is
|
|
||||||
becoming</em>. Not what has been, and not what is fated to be: the thing you are
|
|
||||||
in the middle of. Which is what a to-do list is.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<a class="button primary" href="https://kristianwind.github.io/verdande/self-hosting/">
|
<a class="button primary" href="https://kristianwind.github.io/verdande/self-hosting/">
|
||||||
@@ -553,6 +803,14 @@
|
|||||||
<a class="button" href="https://github.com/kristianwind/verdande">Source</a>
|
<a class="button" href="https://github.com/kristianwind/verdande">Source</a>
|
||||||
<a class="button" href="https://kristianwind.github.io/verdande/">Documentation</a>
|
<a class="button" href="https://kristianwind.github.io/verdande/">Documentation</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ul class="facts">
|
||||||
|
<li>One Go binary</li>
|
||||||
|
<li>SQLite inside it</li>
|
||||||
|
<li>amd64 and arm64</li>
|
||||||
|
<li>No account anywhere but yours</li>
|
||||||
|
<li>MIT</li>
|
||||||
|
</ul>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- No screenshot: the quick-add parser is the thing worth showing, and it
|
<!-- No screenshot: the quick-add parser is the thing worth showing, and it
|
||||||
@@ -584,7 +842,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section>
|
<section id="screens">
|
||||||
<h2>What it looks like</h2>
|
<h2>What it looks like</h2>
|
||||||
<figure class="shot only-dark">
|
<figure class="shot only-dark">
|
||||||
<img
|
<img
|
||||||
@@ -669,11 +927,43 @@
|
|||||||
/>
|
/>
|
||||||
<figcaption>What has been done on the server, across every project.</figcaption>
|
<figcaption>What has been done on the server, across every project.</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
<figure class="shot">
|
||||||
|
<img
|
||||||
|
src="screenshots/delegated.webp"
|
||||||
|
alt="Waiting on others: tasks that have been handed to somebody else, grouped under the person who has them."
|
||||||
|
width="1400"
|
||||||
|
height="900"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<figcaption>Waiting on others — grouped by who has it.</figcaption>
|
||||||
|
</figure>
|
||||||
|
<figure class="shot">
|
||||||
|
<img
|
||||||
|
src="screenshots/notes-source.webp"
|
||||||
|
alt="A note with the source view open: the same note as Markdown, with headings, a list, a quotation, a wiki link and a fenced code block."
|
||||||
|
width="1400"
|
||||||
|
height="900"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<figcaption>The same note as Markdown. What is on disk is what you see.</figcaption>
|
||||||
|
</figure>
|
||||||
|
<figure class="shot">
|
||||||
|
<img
|
||||||
|
src="screenshots/settings.webp"
|
||||||
|
alt="The integrations settings page: connecting Gmail with an OAuth client, and below it the calendar feed address."
|
||||||
|
width="1400"
|
||||||
|
height="900"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<figcaption>Integrations: your mailbox, your calendar feed, your address.</figcaption>
|
||||||
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section id="features">
|
||||||
<h2>What it does</h2>
|
<h2>What it does</h2>
|
||||||
|
|
||||||
|
<p class="group-label">Doing the work</p>
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="cell">
|
<div class="cell">
|
||||||
<h3>Reads what you type</h3>
|
<h3>Reads what you type</h3>
|
||||||
@@ -683,20 +973,54 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="cell">
|
<div class="cell">
|
||||||
<h3>Shared, properly</h3>
|
<h3>Nothing to remember</h3>
|
||||||
<p>
|
<p>
|
||||||
Owner, editor and viewer roles. People outside your instance join by
|
Start typing anywhere and you are writing a task, with the letter you
|
||||||
invite link. Changes appear live.
|
pressed already in the field. <code>⌘K</code> goes anywhere else.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<h3>Search that reads Danish</h3>
|
||||||
|
<p>
|
||||||
|
Type <code>gron</code> and find <code>grøn</code>. Unicode does not
|
||||||
|
consider <code>ø</code> an accented <code>o</code>, so a search that
|
||||||
|
leans on that alone is broken for the people this was built for.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<h3>Repeats, and says so in words</h3>
|
||||||
|
<p>
|
||||||
|
Recurring tasks are RRULE underneath and "every other Tuesday" on the
|
||||||
|
screen. Reminders are absolute or relative, and go out to the devices
|
||||||
|
you have allowed them on.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="cell">
|
<div class="cell">
|
||||||
<h3>Notes, beside the work</h3>
|
<h3>Notes, beside the work</h3>
|
||||||
<p>
|
<p>
|
||||||
Rich text to write in, Markdown on disk. File a note in a project and
|
Rich text to write in, Markdown on disk — and a source view to see it.
|
||||||
everybody in it can read it; <code>#project</code> and
|
Images are pasted or dragged in. <code>#project</code> and
|
||||||
<code>[[another note]]</code> are links, and a task shows what has been
|
<code>[[another note]]</code> are links, and a task shows what has been
|
||||||
written about it. A zip of Markdown goes in and comes back out the same
|
written about it. Archive what is done with it; the bin is for mistakes.
|
||||||
shape, so nothing is trapped here.
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<h3>Waiting on others</h3>
|
||||||
|
<p>
|
||||||
|
What you have handed to somebody else, on its own page, grouped under
|
||||||
|
the person who has it. The thing a shared list stops being able to tell
|
||||||
|
you the moment it is shared.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="group-label">Connected to what you already use</p>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="cell">
|
||||||
|
<h3>Shared, properly</h3>
|
||||||
|
<p>
|
||||||
|
Owner, editor and viewer roles. People outside your instance join by
|
||||||
|
invite link. Changes appear live.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="cell">
|
<div class="cell">
|
||||||
@@ -706,6 +1030,27 @@
|
|||||||
here. Not a read-only feed pretending.
|
here. Not a read-only feed pretending.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<h3>A calendar of its own</h3>
|
||||||
|
<p>
|
||||||
|
A month or a week of what is due, and a secret ICS address to subscribe
|
||||||
|
to from Apple Calendar, Google or Thunderbird. A Google calendar can be
|
||||||
|
laid over the top, read-only.
|
||||||
|
</p>
|
||||||
|
<p class="caveat">
|
||||||
|
The Google half is written to Google's documentation and has not yet
|
||||||
|
been run against Google. The feed and CalDAV have.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<h3>Your mail, as an inbox</h3>
|
||||||
|
<p>
|
||||||
|
Star a mail in Gmail, or flag one over IMAP in iCloud or Fastmail, and
|
||||||
|
it becomes a task. Forward one to your own address instead and the
|
||||||
|
subject is parsed like anything else you type. One way: unstarring it
|
||||||
|
later does nothing to the task you made.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div class="cell">
|
<div class="cell">
|
||||||
<h3>Claude, connected</h3>
|
<h3>Claude, connected</h3>
|
||||||
<p>
|
<p>
|
||||||
@@ -714,35 +1059,39 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="cell">
|
<div class="cell">
|
||||||
<h3>Your mail, as an inbox</h3>
|
<h3>AI only if you ask for it</h3>
|
||||||
<p>
|
<p>
|
||||||
Star a mail in Gmail, or flag one in iCloud or Fastmail, and it becomes
|
Off by default, your own key, and your choice of Anthropic, OpenAI,
|
||||||
a task. Forward one to your own address instead and the subject is
|
Google or something local. Nothing leaves the server until you have
|
||||||
parsed like anything else you type. One way: unstarring it later does
|
filled that in, and everything works without it.
|
||||||
nothing to the task you made.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="group-label">Yours, and checkable</p>
|
||||||
|
<div class="grid">
|
||||||
<div class="cell">
|
<div class="cell">
|
||||||
<h3>Nothing to remember</h3>
|
<h3>More than one way in</h3>
|
||||||
<p>
|
<p>
|
||||||
Start typing anywhere and you are writing a task, with the letter you
|
Passkeys, or a password hashed with Argon2id and a second factor with
|
||||||
pressed already in the field. Sign in with a passkey and there is no
|
recovery codes. Every device you are signed in on is listed, and any of
|
||||||
password to forget either.
|
them can be signed out from here.
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="cell">
|
|
||||||
<h3>Yours to leave</h3>
|
|
||||||
<p>
|
|
||||||
Import from Todoist, export back to it, or take everything as JSON. The
|
|
||||||
round trip is tested, not promised.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="cell">
|
<div class="cell">
|
||||||
<h3>Keys stay behind</h3>
|
<h3>Keys stay behind</h3>
|
||||||
<p>
|
<p>
|
||||||
Mail tokens and mailbox passwords are encrypted in the database, and the
|
Mail tokens, mailbox passwords, second-factor secrets and AI keys are
|
||||||
key lives in a file beside it — not inside it. So a backup you download,
|
encrypted in the database, and the key lives in a file beside it — not
|
||||||
or one that ends up somewhere it should not, does not open your mail.
|
inside it. So a backup you download, or one that ends up somewhere it
|
||||||
|
should not, does not open your mail.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<h3>Nobody signs themselves up</h3>
|
||||||
|
<p>
|
||||||
|
The first account is the administrator, and after that people arrive by
|
||||||
|
invitation. An address on the internet is not an open door.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="cell">
|
<div class="cell">
|
||||||
@@ -753,50 +1102,85 @@
|
|||||||
instance's record, not the person's.
|
instance's record, not the person's.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<h3>Yours to leave</h3>
|
||||||
|
<p>
|
||||||
|
Import from Todoist, export back to it, take everything as JSON, or
|
||||||
|
hand the notes a zip of Markdown and get the same zip back out. The
|
||||||
|
round trip is tested, not promised.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<h3>It counts itself, out loud</h3>
|
||||||
|
<p>
|
||||||
|
Once a day an instance says it exists — a random id, the version, and
|
||||||
|
nothing else. No tasks, no names, no addresses. It is on to begin with,
|
||||||
|
the settings page prints the exact payload, and one switch stops it.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section id="running">
|
||||||
<h2>Running it</h2>
|
<h2>Running it</h2>
|
||||||
|
|
||||||
<h3 class="sub">As a Rune in Yggdrasil Panel</h3>
|
<div class="panels">
|
||||||
<p class="muted">
|
<div class="panel">
|
||||||
This is what it was built for. <strong>Runes → Carve a rune</strong>, upload
|
<h3>As a Rune in Yggdrasil Panel</h3>
|
||||||
<code>rune/verdande.yaml</code>, create a server from it, set the public address
|
<p>
|
||||||
and your mail server, and start it. No port forwarding — give it a subdomain and
|
This is what it was built for. <strong>Runes → Carve a rune</strong>,
|
||||||
the panel routes it through your Cloudflare Tunnel.
|
upload <code>rune/verdande.yaml</code>, create a server from it, set the
|
||||||
</p>
|
public address and your mail server, and start it. No port forwarding —
|
||||||
|
give it a subdomain and the panel routes it through your Cloudflare
|
||||||
<h3 class="sub">Or with Docker, anywhere</h3>
|
Tunnel.
|
||||||
<pre><code>docker run -d --name verdande \
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="panel">
|
||||||
|
<h3>Or with Docker, anywhere</h3>
|
||||||
|
<p>Open the address afterwards and create the first account. That one is the administrator.</p>
|
||||||
|
<pre><code>docker run -d --name verdande \
|
||||||
-p 8080:8080 \
|
-p 8080:8080 \
|
||||||
-v verdande-data:/data \
|
-v verdande-data:/data \
|
||||||
-e VERDANDE_BASE_URL=https://todo.example.dk \
|
-e VERDANDE_BASE_URL=https://todo.example.dk \
|
||||||
ghcr.io/kristianwind/verdande:latest</code></pre>
|
ghcr.io/kristianwind/verdande:latest</code></pre>
|
||||||
<p class="muted" style="margin-top: 1rem">
|
</div>
|
||||||
Open the address and create the first account. That one is the administrator.
|
</div>
|
||||||
</p>
|
|
||||||
<ul class="plain">
|
<ul class="plain">
|
||||||
<li>No database to provision — SQLite is inside the binary.</li>
|
<li>No database to provision — SQLite is inside the binary.</li>
|
||||||
<li>Distroless image: no shell, no package manager, one static file.</li>
|
<li>Distroless image: no shell, no package manager, one static file.</li>
|
||||||
<li>amd64 and arm64, so a Raspberry Pi is fine.</li>
|
<li>amd64 and arm64, so a Raspberry Pi is fine.</li>
|
||||||
<li>Nightly backups, fourteen kept, written with <code>VACUUM INTO</code>.</li>
|
<li>Nightly backups, fourteen kept, written with <code>VACUUM INTO</code>.</li>
|
||||||
|
<li>Twenty-four migrations, run on start. Upgrading is pulling the new image.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section id="limits">
|
||||||
<h2>What it will not do</h2>
|
<h2>What it will not do</h2>
|
||||||
<p class="muted">
|
<ul class="nogo">
|
||||||
No karma or points. No team workspaces. No native apps. No mail integration
|
<li>No karma, streaks or points.</li>
|
||||||
beyond Gmail and the forwarding address. No languages beyond Danish and English.
|
<li>No team workspaces — projects and the people in them are the whole model.</li>
|
||||||
And no sync protocol of its own — CalDAV and the API <em>are</em> the sync.
|
<li>No native apps. It is a PWA, and it installs on a phone.</li>
|
||||||
</p>
|
<li>No Outlook or Exchange: Microsoft has closed IMAP to passwords and wants its own flow.</li>
|
||||||
|
<li>No languages beyond Danish and English.</li>
|
||||||
|
<li>No sync protocol of its own — CalDAV and the API <em>are</em> the sync.</li>
|
||||||
|
<li>No hosted version. There is nowhere to sign up, and that is the point.</li>
|
||||||
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
|
<!-- The Norn belongs at the end. It was the third paragraph of the hero,
|
||||||
|
where it stood between somebody arriving and the buttons they came
|
||||||
|
for — and it is a closing thought, not an opening one. -->
|
||||||
|
<p class="norn">
|
||||||
|
Verdande is one of the three Norns — the one who spins <em>that which is
|
||||||
|
becoming</em>. Not what has been, and not what is fated to be: the thing you are
|
||||||
|
in the middle of. Which is what a to-do list is.
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
MIT licensed ·
|
MIT licensed ·
|
||||||
<a href="https://github.com/kristianwind/verdande">github.com/kristianwind/verdande</a>
|
<a href="https://github.com/kristianwind/verdande">github.com/kristianwind/verdande</a>
|
||||||
|
· <a href="https://kristianwind.github.io/verdande/">Documentation</a>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Built to run as a Rune in
|
Built to run as a Rune in
|
||||||
|
|||||||
Reference in New Issue
Block a user