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
All checks were successful
Deploy production / deploy (push) Successful in 2m49s
This commit is contained in:
@@ -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
|
`BOOKS_ROOT=/books`. Without a `library` directory there the deploy still succeeds, but files can't
|
||||||
be opened.
|
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
|
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
|
environment before deploying the new route. The deployment preflight checks for
|
||||||
all nine tracker tables and the projection columns.
|
all nine tracker tables and the projection columns.
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ const aspsp = {
|
|||||||
const defaultAccount = value('--account') ?? preset?.account ?? aspsp.name;
|
const defaultAccount = value('--account') ?? preset?.account ?? aspsp.name;
|
||||||
const redirectUrl =
|
const redirectUrl =
|
||||||
process.env.ENABLEBANKING_REDIRECT_URL ??
|
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();
|
const app = await client.application();
|
||||||
console.log(
|
console.log(
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
'derby lens': ChartNoAxesCombined,
|
'derby lens': ChartNoAxesCombined,
|
||||||
'world opinions': Globe2,
|
'world opinions': Globe2,
|
||||||
'language learner': Languages,
|
'language learner': Languages,
|
||||||
'money tracker': Wallet,
|
money: Wallet,
|
||||||
'cs prices': CandlestickChart,
|
'cs prices': CandlestickChart,
|
||||||
'house tracker': House,
|
'house tracker': House,
|
||||||
lists: ListChecks,
|
lists: ListChecks,
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ export const trackers = [
|
|||||||
external: false
|
external: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'money tracker',
|
name: 'money',
|
||||||
subtitle: 'a manual ledger for accounts, bills and utilities',
|
subtitle: 'a manual ledger for accounts, bills and utilities',
|
||||||
href: '/projects/money-tracker',
|
href: '/trackers/money',
|
||||||
external: false
|
external: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ export function getTitle(path: string, environment: SiteEnvironment = null): str
|
|||||||
'/projects/energy': 'Energy',
|
'/projects/energy': 'Energy',
|
||||||
'/projects/game-tracker': 'Game Tracker',
|
'/projects/game-tracker': 'Game Tracker',
|
||||||
'/projects/language-learner': 'Language Learner',
|
'/projects/language-learner': 'Language Learner',
|
||||||
'/projects/money-tracker': 'Money Tracker',
|
|
||||||
'/projects/spotify-beatmap-finder': 'osu! map finder',
|
'/projects/spotify-beatmap-finder': 'osu! map finder',
|
||||||
'/projects/progress': 'Progress',
|
'/projects/progress': 'Progress',
|
||||||
'/projects/world-opinions': 'World Opinions'
|
'/projects/world-opinions': 'World Opinions',
|
||||||
|
'/trackers/money': 'Money'
|
||||||
};
|
};
|
||||||
const cleaned = path.replace(/\/$/, '') || '/';
|
const cleaned = path.replace(/\/$/, '') || '/';
|
||||||
const title =
|
const title =
|
||||||
|
|||||||
@@ -14,9 +14,9 @@
|
|||||||
stored. Session cookies keep you signed in; there are no advertising or third-party tracking
|
stored. Session cookies keep you signed in; there are no advertising or third-party tracking
|
||||||
cookies.
|
cookies.
|
||||||
</p>
|
</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>
|
<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
|
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
|
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
|
only to show the account holders their own finances. It is stored in a database on a server
|
||||||
|
|||||||
10
src/routes/projects/money-tracker/[...rest]/+server.ts
Normal file
10
src/routes/projects/money-tracker/[...rest]/+server.ts
Normal 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;
|
||||||
30
src/routes/projects/money-tracker/[...rest]/redirect.test.ts
Normal file
30
src/routes/projects/money-tracker/[...rest]/redirect.test.ts
Normal 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']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -22,10 +22,11 @@
|
|||||||
You're responsible for activity on your account. Accounts that abuse the site may be removed.
|
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.
|
You can ask for your account to be deleted at any time.
|
||||||
</p>
|
</p>
|
||||||
<h2 class="pt-2 text-lg font-semibold">Money tracker</h2>
|
<h2 class="pt-2 text-lg font-semibold">Money</h2>
|
||||||
<p>
|
<p>
|
||||||
The money tracker is a private tool for the site owner and household. Its bank connection is
|
Money (/trackers/money) is a private tool for the site owner and household. Its bank connection
|
||||||
read-only (account information via Enable Banking) and nothing shown in it is financial advice.
|
is read-only (account information via Enable Banking) and nothing shown in it is financial
|
||||||
|
advice.
|
||||||
</p>
|
</p>
|
||||||
<h2 class="pt-2 text-lg font-semibold">Liability</h2>
|
<h2 class="pt-2 text-lg font-semibold">Liability</h2>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import { error, redirect } from '@sveltejs/kit';
|
|||||||
import type { Actions, PageServerLoad } from './$types';
|
import type { Actions, PageServerLoad } from './$types';
|
||||||
|
|
||||||
export const load: PageServerLoad = async (event) => {
|
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 user = requireAdmin(event);
|
||||||
const userId = user.id;
|
const userId = user.id;
|
||||||
const [
|
const [
|
||||||
@@ -250,7 +250,7 @@
|
|||||||
);
|
);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.download = 'money-tracker-transactions.csv';
|
link.download = 'money-transactions.csv';
|
||||||
link.click();
|
link.click();
|
||||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||||
}
|
}
|
||||||
@@ -276,7 +276,7 @@
|
|||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<div>
|
<div>
|
||||||
<p class="muted">Personal finance</p>
|
<p class="muted">Personal finance</p>
|
||||||
<h1>Money tracker</h1>
|
<h1>Money</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<Button variant="outline" onclick={() => open('import')}
|
<Button variant="outline" onclick={() => open('import')}
|
||||||
@@ -440,7 +440,7 @@
|
|||||||
<p>{filtered.length} of {data.transactions.length} entries</p>
|
<p>{filtered.length} of {data.transactions.length} entries</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<Button variant="outline" href={resolve('/projects/money-tracker/transactions')}
|
<Button variant="outline" href={resolve('/trackers/money/transactions')}
|
||||||
>All transactions</Button
|
>All transactions</Button
|
||||||
><Button variant="outline" onclick={exportCsv} disabled={!data.transactions.length}
|
><Button variant="outline" onclick={exportCsv} disabled={!data.transactions.length}
|
||||||
><Download size={16} /> Export CSV</Button
|
><Download size={16} /> Export CSV</Button
|
||||||
@@ -767,7 +767,7 @@
|
|||||||
|
|
||||||
<Dialog.Root bind:open={dialogOpen}>
|
<Dialog.Root bind:open={dialogOpen}>
|
||||||
<Dialog.Content class="money-dialog block max-h-[90dvh] overflow-y-auto sm:max-w-lg">
|
<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}
|
{#if showActionError && form?.message}<p class="form-error" role="alert">{form.message}</p>{/if}
|
||||||
<Dialog.Header class="mb-6 pr-8"
|
<Dialog.Header class="mb-6 pr-8"
|
||||||
><Dialog.Title>
|
><Dialog.Title>
|
||||||
@@ -14,10 +14,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head
|
<svelte:head
|
||||||
><title>Bank connection · Money tracker</title><meta
|
><title>Bank connection · Money</title><meta name="robots" content="noindex" /></svelte:head
|
||||||
name="robots"
|
|
||||||
content="noindex"
|
|
||||||
/></svelte:head
|
|
||||||
>
|
>
|
||||||
|
|
||||||
<section class="mx-auto max-w-xl space-y-6 p-6">
|
<section class="mx-auto max-w-xl space-y-6 p-6">
|
||||||
@@ -48,6 +45,6 @@
|
|||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
<p>
|
<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>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
else params.set(key, String(value));
|
else params.set(key, String(value));
|
||||||
}
|
}
|
||||||
const query = params.toString();
|
const query = params.toString();
|
||||||
return resolve('/projects/money-tracker/transactions') + (query ? '?' + query : '');
|
return resolve('/trackers/money/transactions') + (query ? '?' + query : '');
|
||||||
}
|
}
|
||||||
function submit(event: SubmitEvent) {
|
function submit(event: SubmitEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
for (const [key, value] of form)
|
for (const [key, value] of form)
|
||||||
if (typeof value === 'string' && value) params.append(key, value);
|
if (typeof value === 'string' && value) params.append(key, value);
|
||||||
const query = params.toString();
|
const query = params.toString();
|
||||||
goto(resolve('/projects/money-tracker/transactions') + (query ? '?' + query : ''), {
|
goto(resolve('/trackers/money/transactions') + (query ? '?' + query : ''), {
|
||||||
keepFocus: true,
|
keepFocus: true,
|
||||||
noScroll: true
|
noScroll: true
|
||||||
});
|
});
|
||||||
@@ -55,14 +55,14 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</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">
|
<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">
|
<header class="flex flex-wrap items-end justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<a
|
<a
|
||||||
class="inline-flex items-center gap-1 text-sm opacity-70 hover:opacity-100"
|
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>
|
<h1 class="text-2xl font-bold">Transactions</h1>
|
||||||
<p class="text-sm opacity-70">
|
<p class="text-sm opacity-70">
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
<div class="flex items-end gap-2">
|
<div class="flex items-end gap-2">
|
||||||
<Button type="submit">Apply</Button><Button
|
<Button type="submit">Apply</Button><Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
href={resolve('/projects/money-tracker/transactions')}>Reset</Button
|
href={resolve('/trackers/money/transactions')}>Reset</Button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -365,7 +365,7 @@ test('shared dialogs have focus, Escape dismissal and mobile layouts without ove
|
|||||||
await expect(dialog).toHaveCount(0);
|
await expect(dialog).toHaveCount(0);
|
||||||
await expect(page.getByRole('button', { name: label, exact: true })).toBeFocused();
|
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.getByRole('heading', { name: 'Accounts', exact: true }).waitFor();
|
||||||
await page.locator('#accounts').getByRole('button', { name: 'Add', exact: true }).click();
|
await page.locator('#accounts').getByRole('button', { name: 'Add', exact: true }).click();
|
||||||
const moneyDialog = page.getByRole('dialog');
|
const moneyDialog = page.getByRole('dialog');
|
||||||
|
|||||||
Reference in New Issue
Block a user