Pages and actions

Every address the app serves a page on, and every form action behind it — read from the route tree, so a page added or an action renamed shows up here on the next build.

An action is what a <form method="post"> posts to. They are the app's own write surface, the way the endpoints in the API are the write surface for everything else; both end up calling the same services.

44 pages, 173 actions.

Page Actions
/ createDiaryEntry, saveWins, setLayout, resetLayout
/admin unban, blockForever, unblockForever, dismissReport, setRole, retryMail, dismissMail
/admin/[id] setRole, setPlanEnd, grantTrial, resendVerification, deleteAccount, impersonate
/buy
/data/[slug]
/demo default
/goals setTodoStatus, createArea, deleteArea, create, update, setProgress, close, setLinks, remove
/health/habits create, update, delete, logOccurrence, toggleOccurrence, updateOccurrence, deleteOccurrence
/ideas create, update, delete, toggleApplied, updateAppliedNote, toggleFavorite
/kitchen/meals
/kitchen/recipes
/kitchen/recipes/[id]
/legal/privacy
/legal/refunds
/legal/terms
/login signIn, signUp, requestReset, signOut, resetDemo
/login/reset reset
/login/verify resend
/mail/weekly-review/off
/newsletter/confirm
/newsletter/off
/notebooks
/notebooks/[id]
/notebooks/diary create, createWins, update, delete
/notebooks/people create, update, setPicture, removePicture, delete
/offline
/search
/settings/account setWeeklyReviewMail, changeEmail, changePassword, revokeSession, signOutEverywhere, delete
/settings/account/import importTasks, importVault, importAccount
/settings/billing acceptFamilyOffer, declineFamilyOffer, checkout, switchInterval
/settings/family addSeat, withdrawInvite, removeSeat
/settings/instance exportSubscribers, setRegistration, setEmailChange, setClientErrors, createInvite, revokeInvite
/settings/integrations createToken, calendarLink, revokeToken, updateStream, deleteStream, createWebhook, deleteWebhook, reviveWebhook
/settings/integrations/widget connect
/settings/preferences setErrorReports, saveCurrency, saveGridHours, saveMenu, resetMenu, setLayout, resetLayout, addQuote, importQuotes, deleteQuote, setStyle, setTheme, saveWeek
/shopping setCategoryFood, setCategoryShared, renameCategory, deleteCategory, saveCategories, createCategory, create, update, toggleBought, paid, delete, restock, toggleSnoozed
/start checkout
/tasks/activities create, update, toggleActive, delete, createCategory, updateCategory, deleteCategory
/tasks/board setStatus, reorder, schedule, promote, demote, createTodo, setRatings, remind, unremind, editInstance, resolveActivity, deleteInstance, deleteTodo
/tasks/plan create, update, toggleActive, delete, bulkDelete, copyToWeekdays, addCalendar, removeCalendar, applyTemplate, saveScheme, loadScheme, deleteScheme, renameScheme, scheduleTodo, unscheduleBlock, convertRepeat, moveOccurrence, setStatus, suppress, unsuppress, createExceptional, updateExceptional, deleteExceptional, importCsv
/tasks/review saveLines, keepStale, completeStale, dropStale, resolve, carry
/tasks/todo create, update, setStatus, schedule, delete, delegate
/welcome assistantToken, finish
/welcome/password default

What these pages do

Only the ones whose code says — the comment at the top of the route file, and the comment above each action. Write it there and it turns up here; the rest are named for what they do.

/admin

What the demo may see here, and what it may not.

The demo signs every visitor into its one account, which is an administrator — so this page is public there, deliberately: somebody deciding whether to run this themselves should see what administering it looks like. What they must not see is anything about the box or about other people: the addresses fail2ban turned away are real people's, a failed mail carries a real address, and a client error carries a stack from the server.

Writes are refused in hooks.server.ts, in one place, for the same reason this list is here rather than spread through the page.

/admin/[id]

setPlanEnd

The operator's clock: end the plan on a chosen date.

grantTrial

A trial for an account that predates billing — see grantTrial.

resendVerification

Send the confirmation link again, for somebody who never got the first.

deleteAccount

Erase the account, having been made to type its address.

A redirect on success rather than a form message: the page this action belongs to is about an account that no longer exists, and re-rendering it would be a 404 or, worse, a cached shell of somebody who is gone.

impersonate

Sign in as somebody else, with the session marked as borrowed.

Recorded against both accounts before it happens: the point of the log is that the person whose account it is can see it too.

/buy

The one page that loads the payment provider's script.

A checkout action mints a transaction and lands here with ?_ptxn=…; Paddle.js reads that itself and opens its overlay. The page is only the overlay's backdrop — the terms were already read on /start or the billing page, so nothing here competes with the payment window.

/demo

The demo's front door.

Making a demo account is not free: a row, a password hash, and a week of seeded data written to SQLite. That took a few seconds while the browser showed nothing at all — a white page, then the dashboard — which reads as a slow site rather than as something being built for you.

So the wait has a screen. It also has somewhere to go: with the work behind a page the visitor is already looking at, the seed can be made slower under load without anybody experiencing a hang.

/kitchen/meals

The seven days from a date, as YYYY-MM-DD.

/kitchen/recipes

Every recipe, with the picture that stands for it.

One query for the whole list rather than one per card: a cookbook is a page of forty cards, and forty round trips to ask "does this one have a picture" is how a list stops being instant.

/login

signUp

Register, if this instance is taking anybody.

The check is repeated here rather than left to the hook in hooks.server.ts: this action calls better-auth in-process, so the request never passes the hook. Two doors, one rule.

requestReset

Ask for a reset link.

Always reports the same thing whether or not the address exists — the response is otherwise a way to enumerate who has an account here.

/login/verify

Signed in, address unconfirmed — the one page such an account can reach while ONTOPLANO_REQUIRE_VERIFIED_EMAIL=true (the gate is in hooks.server.ts). It says so plainly and offers exactly one act: sending the mail again.

/mail/weekly-review/off

The link at the bottom of the weekly review, and the only thing it does.

Reachable with no session on purpose: somebody who has stopped opening the app is exactly who this mail is for, and asking them to sign in before they can make it stop is how a lifecycle mail becomes a complaint. The signature in the URL is what makes naming an account in it safe — without one, this would be a way to turn off anybody's mail by guessing an id.

It turns off one thing and says so. It cannot turn anything on, so a link that leaks does no more harm than the click it was already for, and there is nothing here worth replaying.

/newsletter/confirm

The second half of the double opt-in, and the only thing that puts an address on the list.

Signed out by definition: the person following it has no account here and may never have one. A 404 for a token that is not one, rather than a message — the difference between "wrong token" and "no such token" is the only thing an enumerator would learn.

/newsletter/off

The link at the bottom of every issue.

One click, no session, no confirmation step. A way out with a form in front of it is the thing that makes somebody press the spam button instead, which costs the domain far more than the address ever did.

/notebooks

The query value that stands for the orphaned notes rather than a notebook.

/notebooks/[id]

One notebook, with nothing else on the page.

The index shows a notebook beside the list of them, which is the right shape for moving between subjects and the wrong one for sitting inside a single one. This is the same notebook with the whole width.

/offline

Public on purpose.

The layout guard sends signed-out visitors to /login, but this page is what a failed navigation falls back to — including one where the session could not be checked because there is no network.

/settings/account

better-auth's messages are already user-facing; anything else is a bug.

changeEmail

Ask to move the account to another address, where the instance allows it.

Nothing changes here: better-auth mails a confirmation and the swap happens when the link is followed. The current address stays the one that signs in until then.

changePassword

Change the password, and sign every other session out.

Someone changing their password is often doing it because they think somebody else is logged in, so the other sessions go with it.

revokeSession

Sign one device out. Its next request finds nothing to authenticate with.

signOutEverywhere

Sign out everywhere, this browser included.

Keeping the current session alive would be friendlier and wrong: someone clicking this has usually decided they do not know who else is logged in, and the answer to that is nobody.

delete

Deleting an account is irreversible, so it asks for the account's own email address rather than a yes/no — the point is to make it impossible to do by reflex, not to add a step.

/settings/account/import

Bringing things in, on a page of its own.

It used to be two cards at the bottom of the account page, below the sessions and above the delete button — which put "restore an export over everything you have" three inches from "change your password". Moving in is its own act, done once, and it reads better as its own page than as the tail of somebody else's.

importTasks

Take a list out of Todoist, Google Tasks or Google Keep and put it here.

The parsing and the writing are services/imports.ts; this reads the form. The text arrives in the textarea whether it was pasted or read from a chosen file — the page reads the file itself, so what is about to be imported is visible before the button is pressed, and no file is ever uploaded.

importVault

A vault of markdown becomes notebook entries.

The files arrive as one JSON array of { path, text }, read in the page: the browser can hand over a whole folder, and nothing is uploaded as a file — the same arrangement the task import uses, for the same reason. The path matters as well as the text, because a vault's folders are structure and they come across as tags.

importAccount

Put an exported account back \u2014 into this one, over what is here.

Destructive, so it asks for a typed word rather than a click: this empties the account before it fills it, and the one thing worse than an import that fails is an import that half-succeeds over a real week. importAccount is one transaction for the same reason.

/settings/billing

What this account is on, and what it is using.

A self-hosted instance sells nothing, so the page says so and stops — the same answer the deployment settings give.

/settings/family

Who else is on this plan.

This was a card at the bottom of Billing, which put the one thing a family payer comes back for — adding the fourth person, months later — underneath the prices and the usage bars. It is its own tab now, and the tab only exists for an account that is on a family plan: the payer, or somebody whose seat is on somebody else's.

/settings/instance

Deployment settings: bind host, port, database path, and who may register.

These describe the machine, not the account, so they are only readable and writable on a self-hosted instance by its owner. Anyone else gets a 404 — "not yours" and "not there" are the same answer. This page moves to /admin once roles land.

/settings/integrations

calendarLink

Mint another calendar link.

Several are allowed — a phone, a laptop, a partner's calendar — because one per account meant that wanting it in a second place cost you the first. Each is revoked on its own, in the list below, which is what makes "I pasted that one somewhere I should not have" recoverable without breaking the calendars that are fine.

/settings/integrations/widget

Where the phone widget connects itself.

The widget used to ask for an address and a pasted token, which is asking a person to do a key exchange by hand. Now its Connect button opens this page in the browser — where a session already exists — one tap mints the key, and the app link on the way back carries it home. Nobody sees a token.

/settings/preferences

Everything on this page belongs to the account, never to the instance (I9).

saveMenu

The menu: its order, what is put away, and the colours. One form.

Three settings, saved together, because they are three answers about the same eight rooms and splitting them into three forms is what made the page list everything three times.

room arrives in the order the list showed, hidden names the ones put away, and color.<section> carries a hex per section. Each is validated where it is stored — see $lib/nav-order.ts and server/settings.ts — because what counts as a room and what counts as a colour both change independently of this form.

resetMenu

Back to the order, the colours and the sections the app ships with.

/shopping

Every action here is the same shape: read the form, call the service, map errors.

setCategoryFood

One tick, saved as it lands — the modal has no save button any more.

setCategoryShared

The owner's switch: the family sees the section and fills it.

createCategory

Making a category is its own act, and needs its own action.

It used to be a second pair of fields inside saveCategories, so one Save meant two things. Splitting the form was right and left this behind: the new form posted here and there was nothing here to post to, so the dialog simply did nothing and said nothing about it.

paid

What you actually paid. Never part of the tick, which has to stay one press.

/start

The card step of the funnel: register → confirm → here.

The billing-hold gate in hooks.server.ts routes a verified account with no plan yet to this page, so the terms are read BEFORE any payment window opens — /buy is just the overlay's backdrop. Yearly leads; it is the one worth taking.

/tasks/board

Next sensible start time on a day.

Today gets the next half hour from now so a promoted todo lands ahead of you rather than in the past; another day starts at nine.

reorder

Where a card sits within its column.

Only todos have a position to remember — an occurrence's place is its time of day, and letting a drag override that would put the board and the calendar into disagreement over the same task.

schedule

Dragging between the two tabs: a todo gains a day, or gives one up.

promote

Turn a todo into a real scheduled task.

A todo pulled onto a day stops being a todo: it becomes a one-off block with a time, which is what makes it show up on the grid, in the tracker, and against a goal. The row moves rather than being copied, so there is never a todo and a task that are secretly the same thing.

demote

The reverse: a one-off goes back to being an undated todo.

remind

"Remind me before this one." A lead time, not a clock reading.

resolveActivity

Which activity a category-shaped block turned out to be.

/tasks/plan

What the browser last knew about its own width.

Not a preference and not a setting — a fact about the screen, written by the page that can see one, read by the render that cannot.

scheduleTodo

Drop an undated todo onto the grid: it becomes a block at that hour.

unscheduleBlock

A block dragged off the grid and back onto the todo strip.

The reverse of scheduleTodo, and the reason the strip is a drop target: scheduling used to be one-way, so changing your mind meant deleting the block and typing it in again.

/tasks/review

Last week by default.

You review a week once it is over; landing on the current one would invite writing three lines about a Wednesday.

resolve

Done, or skipped — the two answers that are not "carry it forward".

/welcome/password

The one page between a family invitation's link and the app.

The account it opens was made with a password nobody knows, so the first thing to do with it is choose one — before the welcome wizard, because a person who closes the tab after setup would otherwise own an account they cannot get back into except through a password reset they don't know to ask for.