{#snippet signed(value: number)} 0} class:negative={value < 0}>{amount(value)} {/snippet} {#snippet swatch(id: number | null)} {/snippet} {#snippet rowActions(label: string, edit: () => void, remove: () => void)} {/snippet}

Personal finance

Money

Net worth · {totalAccounts.length} accounts

{@render signed(total)}

Spendable

{@render signed(total - invested)}

Invested

{@render signed(invested)}

Net this month

{@render signed(monthNet)}

Recurring / month

{@render signed(Math.round(monthlyRecurring))}

Projected {dateLabel(projection[0].points.at(-1)?.date ?? data.today)}

{@render signed(projectionEnd[0])}

{#if importNotice}

{importNotice}

{/if}

Wealth over time

Actual balances through today, then the projection.

{#each timelineViews as item (item.id)}{item.label}{/each} {#each historyRanges as item (item.id)}{item.label}{/each}
Total at end{@render signed(projectionEnd[0])}
Spendable at end{@render signed(projectionEnd[1])}
Invested at end{@render signed(projectionEnd[2])}

Includes future dated transactions and estimates{data.projectionSettings ?.includeRecurring !== false ? ', plus recurring monthly equivalents from next month' : ''}. {data.projectionSettings?.investmentAccountId ? `The selected investment account and new contributions use a hypothetical ${data.projectionSettings.annualReturnPercent}% annual return.` : `Excess above ${amount(data.projectionSettings?.reserveCents ?? 100000)} moves into a projected investment balance with a hypothetical ${data.projectionSettings?.annualReturnPercent ?? '5.00'}% annual return.`} Contributions move money between spendable and invested balances. Enter an item only once across transactions, estimates, and recurring items to avoid counting it twice.

Accounts

Select accounts to filter transactions.

{#if data.accounts.length}
    {#each data.accounts as account (account.id)}
  • {@render signed(balances.get(account.id) ?? 0)}{@render rowActions( account.name, () => open('account', account), () => deletion(account.name, account.id, 'deleteAccount') )}
  • {/each}
{:else}

Add an account or import your transactions to get started.

{/if}

Transactions

{filtered.length} of {data.transactions.length} entries

(pageNumber = 1)} /> (pageNumber = 1)} >
{#if data.accounts.length}
{#each data.accounts as account (account.id)}{/each}
{/if}
{#each shown as row (row.id)} data.today} >{/each}
DateDescriptionAccountAmountActions
{dateLabel(row.date)}{row.description}{#if row.source !== 'manual'} {row.source === 'enablebanking' ? 'bank' : row.source}{/if}{@render signed(row.amountCents)}
{#if !shown.length}

No matching transactions.

{/if}{#if filtered.length > 25}{/if}

Recurring

{@render signed(Math.round(monthlyRecurring))} per month now. Feeds the projection when enabled.

{#if data.recurring.length}
    {#each data.recurring as row (row.id)}
  • {@render swatch(row.accountId)}{row.name}{row.cadence} · {row.accountId ? accountName(row.accountId) : 'Any account'} · {row.startsOn ? dateLabel(row.startsOn) : 'Now'} – {row.endsOn ? dateLabel(row.endsOn) : 'ongoing'}{@render signed(row.amountCents)}{@render rowActions( row.name, () => open('recurring', row), () => deletion(row.name, row.id, 'deleteRecurring') )}
  • {/each}
{:else}

Rent, subscriptions, salary and other known repeats.

{/if}

Future estimates

Dated income and costs you expect.

{#if data.forecasts.length}
    {#each data.forecasts as row (row.id)}
  • {@render swatch(row.accountId)}{row.description}{dateLabel(row.date)} · {row.accountId ? accountName(row.accountId) : 'Unassigned'}{@render signed(row.amountCents)}{@render rowActions( row.description, () => open('forecast', row), () => deletion(row.description, row.id, 'deleteForecast') )}
  • {/each}
{:else}

Add expected costs or income to look ahead.

{/if}

Utilities

Meter readings and estimated variable cost.

{#each kinds as kind (kind.id)}{/each}
Usage{estimate ? estimate.usage.toLocaleString('en-NL', { maximumFractionDigits: 3 }) : '—'} {utility.unit}
Est. cost{estimate ? amount(estimate.estimatedCents) : '—'}
Advances − cost{#if estimate}{@render signed(estimate.differenceCents)}{:else}—{/if}
{#if readings.length}
    {#each [...readings].reverse() as row (row.id)}
  • {dateLabel(row.date)}{Number(row.reading).toLocaleString('en-NL', { maximumFractionDigits: 3 })} {utility.unit}{@render rowActions( 'reading from ' + row.date, () => open('reading', row), () => deletion( utility.label + ' reading on ' + dateLabel(row.date), row.id, 'deleteReading' ) )}
  • {/each}
{:else}

Add at least two readings to calculate usage.

{/if}

Uses the first and last readings in the billing period, the current unit rate and prorated monthly advances. Fixed charges, taxes and settlements are not included.

Wish list

Cumulative cost by priority tier.

{#each wishTiers as row (row.tier)}
{row.tier === 5 ? 'Tier 5' : `5–${row.tier}`}€ {compactAmount(row.value)}{row.count} {row.count === 1 ? 'item' : 'items'}
{/each}
{#if unpricedWishes}

{unpricedWishes} unpriced {unpricedWishes === 1 ? 'item is' : 'items are'} excluded from these totals.

{/if} {#if data.wishlist.length}
    {#each sortedWishlist as row (row.id)}
  • {row.priority}{row.name}{#if row.notes}{row.notes}{/if}{row.estimatedCents == null ? '—' : amount(row.estimatedCents)}{@render rowActions( row.name, () => open('wish', row), () => deletion(row.name, row.id, 'deleteWishlist') )}
  • {/each}
{:else}

Nothing on your wish list yet.

{/if}

Insurance

{amount(Math.round(monthlyInsurance))} per month in premiums.

{#if data.insurances.length}
    {#each data.insurances as row (row.id)}
  • {row.name}{[row.provider, row.coverage].filter(Boolean).join(' · ') || '—'}{row.premiumCents == null ? '—' : amount(row.premiumCents) + ' / ' + row.cadence}{@render rowActions( row.name, () => open('insurance', row), () => deletion(row.name, row.id, 'deleteInsurance') )}
  • {/each}
{:else}

Add policies to keep track of what is covered.

{/if}
Manage your money records. {#if showActionError && form?.message}{/if} {mode === 'delete' ? 'Delete item' : mode === 'import' ? 'Import transactions' : mode === 'settings' ? 'Utility settings' : mode === 'projectionSettings' ? 'Wealth projection' : (active.id ? 'Edit ' : 'Add ') + (mode === 'wish' ? 'wish list item' : mode === 'reading' ? 'meter reading' : mode)} {#if mode === 'delete'}

Delete {active.name}? This cannot be undone.

async ({ result, update }) => { await update(); showActionError = result.type === 'failure'; if (result.type === 'success') dialogOpen = false; }} >
{:else if mode === 'import'}

Upload your CSV or tab-separated export with date, account, description, amount columns, a Revolut statement export (only completed EUR rows are imported, fees included), or Knab exports. Knab files go to the account with that IBAN; pick an account to merge several Knab accounts (current + savings) into one, and transfers between them are left out. Dates like Fri 1 Aug 2025 and amounts like -€1,234.50 are supported. Empty rows are skipped, and entries appear in date order. “Start balance” rows import as transactions, so leave those accounts’ opening balances at zero. New account names are created automatically. Rows already imported, or already entered with the same amount within a day, are skipped.

async ({ result, update }) => { await update(); showActionError = result.type === 'failure'; if (result.type === 'success') { dialogOpen = false; const done = result.data as | { imported?: number; duplicates?: number; internal?: number; replaced?: number } | undefined; importNotice = `Imported ${done?.imported ?? 0} transactions; skipped ${done?.duplicates ?? 0} duplicates` + (done?.internal ? `, ${done.internal} transfers between your own accounts` : '') + (done?.replaced ? `; replaced ${done.replaced} hand entries that were sums of payments` : '') + '.'; } }} >
{:else}
async ({ result, update }) => { await update({ reset: false }); showActionError = result.type === 'failure'; if (result.type === 'success') dialogOpen = false; }} > {#if active.id}{/if} {#if mode === 'account'} {:else if mode === 'transaction' || mode === 'forecast'} {:else if mode === 'recurring'} {:else if mode === 'projectionSettings'}

A planning scenario. Returns are hypothetical and calculated monthly. Contributions transfer money from spendable accounts into a projected investment balance.

{:else if mode === 'settings'} {:else if mode === 'reading'} {:else if mode === 'wish'}