Money: move /projects/money-tracker to /trackers/money, rename to Money, 308 redirects from old paths (bank-callback query kept)
All checks were successful
Deploy production / deploy (push) Successful in 2m49s

This commit is contained in:
2026-10-24 02:37:07 +00:00
parent 2af279e8a9
commit 988c6c31a9
17 changed files with 67 additions and 29 deletions

View File

@@ -260,9 +260,9 @@ bind-mounts the host's `BOOKS_HOST_DIR` (default `/home/stan/books`) read-only a
`BOOKS_ROOT=/books`. Without a `library` directory there the deploy still succeeds, but files can't
be opened.
## Money tracker
## Money
`/projects/money-tracker` is restricted to administrator accounts. Its tables
`/trackers/money` (formerly `/projects/money-tracker`, which now redirects) is restricted to administrator accounts. Its tables
are created by `npm run db:migrate:money-tracker`; apply that migration to each
environment before deploying the new route. The deployment preflight checks for
all nine tracker tables and the projection columns.

View File

@@ -69,7 +69,7 @@ const aspsp = {
const defaultAccount = value('--account') ?? preset?.account ?? aspsp.name;
const redirectUrl =
process.env.ENABLEBANKING_REDIRECT_URL ??
'https://stanrunge.dev/projects/money-tracker/bank-callback';
'https://stanrunge.dev/projects/money-tracker/bank-callback'; // registered with Enable Banking; 308s to /trackers/money/bank-callback with the query kept
const app = await client.application();
console.log(

View File

@@ -33,7 +33,7 @@
'derby lens': ChartNoAxesCombined,
'world opinions': Globe2,
'language learner': Languages,
'money tracker': Wallet,
money: Wallet,
'cs prices': CandlestickChart,
'house tracker': House,
lists: ListChecks,

View File

@@ -41,9 +41,9 @@ export const trackers = [
external: false
},
{
name: 'money tracker',
name: 'money',
subtitle: 'a manual ledger for accounts, bills and utilities',
href: '/projects/money-tracker',
href: '/trackers/money',
external: false
},
{

View File

@@ -34,10 +34,10 @@ export function getTitle(path: string, environment: SiteEnvironment = null): str
'/projects/energy': 'Energy',
'/projects/game-tracker': 'Game Tracker',
'/projects/language-learner': 'Language Learner',
'/projects/money-tracker': 'Money Tracker',
'/projects/spotify-beatmap-finder': 'osu! map finder',
'/projects/progress': 'Progress',
'/projects/world-opinions': 'World Opinions'
'/projects/world-opinions': 'World Opinions',
'/trackers/money': 'Money'
};
const cleaned = path.replace(/\/$/, '') || '/';
const title =

View File

@@ -14,9 +14,9 @@
stored. Session cookies keep you signed in; there are no advertising or third-party tracking
cookies.
</p>
<h2 class="pt-2 text-lg font-semibold">Bank data (money tracker)</h2>
<h2 class="pt-2 text-lg font-semibold">Bank data (Money)</h2>
<p>
The money tracker is a private tool used only by the site owner. With the account holders'
Money (/trackers/money) is a private tool used only by the site owner. With the account holders'
explicit consent, it reads account details, balances and booked transactions from Revolut
through Enable Banking Oy, a licensed account information service provider. This data is used
only to show the account holders their own finances. It is stored in a database on a server

View File

@@ -0,0 +1,10 @@
import { redirect } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
// Money moved from /projects/money-tracker to /trackers/money. Old links, and the Enable Banking
// redirect URL registered as /projects/money-tracker/bank-callback, keep working: 308 keeps the
// method, the subpath and the query string (?range=, ?code=&state=).
export const fallback: RequestHandler = ({ params, url }) => {
redirect(308, `/trackers/money${params.rest ? `/${params.rest}` : ''}${url.search}`);
};
export const GET = fallback;

View File

@@ -0,0 +1,30 @@
import { isRedirect } from '@sveltejs/kit';
import { describe, expect, it } from 'vitest';
import { fallback } from './+server';
function target(path: string, rest: string) {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fallback({ params: { rest }, url: new URL(`https://stanrunge.dev${path}`) } as any);
} catch (thrown) {
if (isRedirect(thrown)) return [thrown.status, thrown.location];
throw thrown;
}
return null;
}
describe('old money tracker URLs', () => {
it('redirects to /trackers/money, keeping subpath and query', () => {
expect(target('/projects/money-tracker?range=3y', '')).toEqual([
308,
'/trackers/money?range=3y'
]);
expect(target('/projects/money-tracker/transactions?page=2', 'transactions')).toEqual([
308,
'/trackers/money/transactions?page=2'
]);
expect(
target('/projects/money-tracker/bank-callback?state=s&code=abc-123', 'bank-callback')
).toEqual([308, '/trackers/money/bank-callback?state=s&code=abc-123']);
});
});

View File

@@ -22,10 +22,11 @@
You're responsible for activity on your account. Accounts that abuse the site may be removed.
You can ask for your account to be deleted at any time.
</p>
<h2 class="pt-2 text-lg font-semibold">Money tracker</h2>
<h2 class="pt-2 text-lg font-semibold">Money</h2>
<p>
The money tracker is a private tool for the site owner and household. Its bank connection is
read-only (account information via Enable Banking) and nothing shown in it is financial advice.
Money (/trackers/money) is a private tool for the site owner and household. Its bank connection
is read-only (account information via Enable Banking) and nothing shown in it is financial
advice.
</p>
<h2 class="pt-2 text-lg font-semibold">Liability</h2>
<p>

View File

@@ -31,7 +31,7 @@ import { error, redirect } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => {
if (!event.locals.user) redirect(303, '/login?returnTo=/projects/money-tracker');
if (!event.locals.user) redirect(303, '/login?returnTo=/trackers/money');
const user = requireAdmin(event);
const userId = user.id;
const [

View File

@@ -250,7 +250,7 @@
);
const link = document.createElement('a');
link.href = url;
link.download = 'money-tracker-transactions.csv';
link.download = 'money-transactions.csv';
link.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
@@ -276,7 +276,7 @@
<header class="app-header">
<div>
<p class="muted">Personal finance</p>
<h1>Money tracker</h1>
<h1>Money</h1>
</div>
<div class="header-actions">
<Button variant="outline" onclick={() => open('import')}
@@ -440,7 +440,7 @@
<p>{filtered.length} of {data.transactions.length} entries</p>
</div>
<div class="header-actions">
<Button variant="outline" href={resolve('/projects/money-tracker/transactions')}
<Button variant="outline" href={resolve('/trackers/money/transactions')}
>All transactions</Button
><Button variant="outline" onclick={exportCsv} disabled={!data.transactions.length}
><Download size={16} /> Export CSV</Button
@@ -767,7 +767,7 @@
<Dialog.Root bind:open={dialogOpen}>
<Dialog.Content class="money-dialog block max-h-[90dvh] overflow-y-auto sm:max-w-lg">
<Dialog.Description class="sr-only">Manage your money tracker records.</Dialog.Description>
<Dialog.Description class="sr-only">Manage your money records.</Dialog.Description>
{#if showActionError && form?.message}<p class="form-error" role="alert">{form.message}</p>{/if}
<Dialog.Header class="mb-6 pr-8"
><Dialog.Title>

View File

@@ -14,10 +14,7 @@
</script>
<svelte:head
><title>Bank connection · Money tracker</title><meta
name="robots"
content="noindex"
/></svelte:head
><title>Bank connection · Money</title><meta name="robots" content="noindex" /></svelte:head
>
<section class="mx-auto max-w-xl space-y-6 p-6">
@@ -48,6 +45,6 @@
</p>
{/if}
<p>
<a class="underline" href={resolve('/projects/money-tracker')}>Back to the money tracker</a>
<a class="underline" href={resolve('/trackers/money')}>Back to Money</a>
</p>
</section>

View File

@@ -36,7 +36,7 @@
else params.set(key, String(value));
}
const query = params.toString();
return resolve('/projects/money-tracker/transactions') + (query ? '?' + query : '');
return resolve('/trackers/money/transactions') + (query ? '?' + query : '');
}
function submit(event: SubmitEvent) {
event.preventDefault();
@@ -45,7 +45,7 @@
for (const [key, value] of form)
if (typeof value === 'string' && value) params.append(key, value);
const query = params.toString();
goto(resolve('/projects/money-tracker/transactions') + (query ? '?' + query : ''), {
goto(resolve('/trackers/money/transactions') + (query ? '?' + query : ''), {
keepFocus: true,
noScroll: true
});
@@ -55,14 +55,14 @@
}
</script>
<svelte:head><title>Transactions · Money tracker</title></svelte:head>
<svelte:head><title>Transactions · Money</title></svelte:head>
<div class="mx-auto max-w-screen-xl space-y-6 px-4 py-8 sm:px-6 sm:py-10">
<header class="flex flex-wrap items-end justify-between gap-4">
<div>
<a
class="inline-flex items-center gap-1 text-sm opacity-70 hover:opacity-100"
href={resolve('/projects/money-tracker')}><ArrowLeft size={14} /> Money tracker</a
href={resolve('/trackers/money')}><ArrowLeft size={14} /> Money</a
>
<h1 class="text-2xl font-bold">Transactions</h1>
<p class="text-sm opacity-70">
@@ -138,7 +138,7 @@
<div class="flex items-end gap-2">
<Button type="submit">Apply</Button><Button
variant="ghost"
href={resolve('/projects/money-tracker/transactions')}>Reset</Button
href={resolve('/trackers/money/transactions')}>Reset</Button
>
</div>
</form>

View File

@@ -365,7 +365,7 @@ test('shared dialogs have focus, Escape dismissal and mobile layouts without ove
await expect(dialog).toHaveCount(0);
await expect(page.getByRole('button', { name: label, exact: true })).toBeFocused();
}
await page.goto('/projects/money-tracker');
await page.goto('/trackers/money');
await page.getByRole('heading', { name: 'Accounts', exact: true }).waitFor();
await page.locator('#accounts').getByRole('button', { name: 'Add', exact: true }).click();
const moneyDialog = page.getByRole('dialog');