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.