way overdue

This commit is contained in:
2026-10-11 17:25:26 -04:00
parent 7ca5f67c1b
commit bcef7cf78a
11 changed files with 2154 additions and 3161 deletions

6
config.js Normal file
View File

@ -0,0 +1,6 @@
export const STATUS_FINISHED = 'STATUS_FINISHED';
export const STATUS_PROCESSING = 'STATUS_PROCESSING';
export const STATUS_IDLE = 'STATUS_IDLE';
export const STATUS_ERROR = 'STATUS_ERROR';
export const MESSAGE_START = 'START';

5073
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,31 +13,28 @@
"generate-pwa-assets": "pwa-assets-generator" "generate-pwa-assets": "pwa-assets-generator"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^6.0.0", "@sveltejs/adapter-auto": "^7.0.1",
"@sveltejs/adapter-node": "^5.2.13", "@sveltejs/adapter-node": "^5.5.4",
"@sveltejs/kit": "^2.16.0", "@sveltejs/kit": "^2.57.1",
"@sveltejs/vite-plugin-svelte": "^5.0.0", "@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.19",
"@vite-pwa/assets-generator": "^1.0.0", "@vite-pwa/assets-generator": "^1.0.2",
"@vite-pwa/sveltekit": "^1.0.0", "@vite-pwa/sveltekit": "^1.1.0",
"daisyui": "^5.0.43", "daisyui": "^5.5.19",
"prettier": "^3.4.2", "prettier": "^3.8.3",
"prettier-plugin-svelte": "^3.3.3", "prettier-plugin-svelte": "^3.5.1",
"svelte": "^5.0.0", "svelte": "^5.55.4",
"vite": "^6.2.6" "vite": "^7.0.1"
}, },
"dependencies": { "dependencies": {
"@auth/sveltekit": "^1.10.0", "@auth/sveltekit": "^1.11.2",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.2.2",
"better-auth": "^1.3.4", "better-auth": "^1.6.5",
"cron": "^4.3.2", "cron": "^4.4.0",
"echarts": "^5.6.0", "echarts": "^6.0.0",
"patchright": "^1.52.5", "patchright": "^1.59.4",
"pg": "^8.16.3", "pg": "^8.20.0",
"postgres": "^3.4.7", "postgres": "^3.4.9",
"puppeteer": "^24.14.0", "tailwindcss": "^4.2.2"
"puppeteer-extra": "^3.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2",
"tailwindcss": "^4.1.11"
} }
} }

View File

@ -571,7 +571,7 @@ export async function getUnderallocatedTransactions() {
export async function getOrphanedTransactions() { export async function getOrphanedTransactions() {
const result = await db` const result = await db`
SELECT budget.name as budget_name, SELECT budget.name as budget_name,
budget_transaction.id as trans_id, budget_transaction.id as budget_transaction_id,
budget_transaction.budget_id as budget_id, budget_transaction.budget_id as budget_id,
budget_transaction.transaction_id as transaction_id, budget_transaction.transaction_id as transaction_id,
budget_transaction.amount as budget_amount, budget_transaction.amount as budget_amount,
@ -586,10 +586,14 @@ export async function getOrphanedTransactions() {
export async function getLast30DaysTransactionsSums() { export async function getLast30DaysTransactionsSums() {
let result = await db` let result = await db`
SELECT cast(date as date) as date, SUM(amount) SELECT cast(date as date) as date,
from transaction SUM(budget_transaction.amount) as total_amount,
where date >= (CURRENT_DATE - interval '30 days') budget.name as budget_name
GROUP BY cast(date as date) from budget_transaction
join transaction on budget_transaction.transaction_id = transaction.id
join budget on budget_transaction.budget_id = budget.id
where cast(date as date) >= (CURRENT_DATE - interval '30 days')
GROUP BY cast(date as date), budget.name
order by date desc; order by date desc;
`; `;
// result = Result [{ id: 1, amount: 100.00, notes: "Recent transaction" }, ...] // result = Result [{ id: 1, amount: 100.00, notes: "Recent transaction" }, ...]

View File

@ -1,3 +1,4 @@
import db from './db.js'; import db from './db.js';
import { chromium } from 'patchright'; import { chromium } from 'patchright';
@ -58,7 +59,7 @@ export async function pullData(amount = 100) {
await new Promise((resolve) => setTimeout(resolve, Math.random() * 5000)); await new Promise((resolve) => setTimeout(resolve, Math.random() * 5000));
await page.getByLabel('Login ID').fill('92830'); await page.getByLabel('Login ID').fill('92830');
await new Promise((resolve) => setTimeout(resolve, Math.random() * 5000)); await new Promise((resolve) => setTimeout(resolve, Math.random() * 5000));
await page.getByLabel('Password').fill('Cmtjlt13'); await page.getByLabel('Password').fill('u3T8F}$*fNdT$bK');
await new Promise((resolve) => setTimeout(resolve, Math.random() * 5000)); await new Promise((resolve) => setTimeout(resolve, Math.random() * 5000));
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
@ -183,29 +184,9 @@ export async function pullData(amount = 100) {
const cardRegEx = /\d{4}$/; const cardRegEx = /\d{4}$/;
// Check if any pending need to be updated // Check if any pending need to be update
state.push(`Checking for pending transactions for account ID: ${account.id}`);
let currentPend =
await db`SELECT id, amount, description, date from transaction where account_id = ${account.id} AND pending=true`;
for (const pend of currentPend) {
const found = transactions.find((t) => `${t.description}:${t.amount}` === pend.id && t.transactionType == "Memo");
if (found && found.transactionType != "Memo")
{state.push(
`I think I found an updated transaction: ${found.statementDescription} ${found.amount} for ${pend.description} ${pend.amount}`
);
await db`UPDATE budget_transaction SET transaction_id = ${found.transactionId} WHERE transaction_id = ${pend.id}`;
await db`DELETE FROM transaction WHERE id = ${pend.id}`;
} else if (!found)
{
state.push(`Orphaning no longer pending budget transaction with no new parent`);
await db`UPDATE budget_transaction SET transaction_id = null, notes = notes || ${pend.description} WHERE transaction_id = ${pend.id}`;
state.push(`Removing pending transaction: ${pend.id}`);
await db`DELETE FROM transaction WHERE id = ${pend.id}`;
}
}
for (const transaction of transactions) { for (const transaction of transactions) {
const amount = Number(transaction.amount); const amount = Number(transaction.amount);
@ -251,6 +232,33 @@ export async function pullData(amount = 100) {
statement_description = EXCLUDED.statement_description, statement_description = EXCLUDED.statement_description,
pending = EXCLUDED.pending`; pending = EXCLUDED.pending`;
} }
state.push(`Checking for pending transactions for account ID: ${account.id}`);
let currentPend =
await db`SELECT id, amount, description, date from transaction where account_id = ${account.id} AND pending=true`;
for (const pend of currentPend) {
const found = transactions.find((t) => `${t.description}:${t.amount}` === pend.id && t.transactionType == "Memo");
if (found)
{
state.push(`Looks like pending ${found.description} $${found.amount} still exsists`);
} else {
const canidate = transactions.find((t) => (t.description.toLowerCase().includes(pend.description.toLowerCase()) && t.amount == pend.amount));
if (canidate)
{
state.push(
`I think I found an updated transaction: ${canidate.statementDescription} ${canidate.amount} for ${pend.description} ${pend.amount}`
);
//await db`UPDATE budget_transaction SET transaction_id = ${canidate.transactionId} WHERE transaction_id = ${pend.id}`;
//await db`DELETE FROM transaction WHERE id = ${pend.id}`;
} else {
state.push(`Orphaning no longer pending budget transaction with no new parent`);
await db`UPDATE budget_transaction SET transaction_id = null, notes = notes || ${pend.description} WHERE transaction_id = ${pend.id}`;
state.push(`Removing pending transaction: ${pend.id}`);
await db`DELETE FROM transaction WHERE id = ${pend.id}`;
}
}
}
} }
/* /*
state.push('Orphaning transactions'); state.push('Orphaning transactions');

52
src/lib/waterfall.svelte Normal file
View File

@ -0,0 +1,52 @@
<script>
import * as charts from 'echarts';
let { last30days, budgets } = $props();
let dailyData = $derived(last30days.filter((e) => e.budget_name === 'Daily'));
let dailyTotal = $derived(budgets.find((b) => b.name === 'Daily')?.sum || 0);
let data = $derived.by(() => {
let accumulated = dailyTotal;
let result = dailyData
.map((entry) => {
accumulated -= parseFloat(entry.total_amount);
return { total: Math.round(accumulated), date: entry.date };
})
.reverse();
result.push({ total: dailyTotal, date: 'Today' });
return result;
});
$inspect(data);
const option = {
xAxis: {
type: 'category',
data: data.map((e) => new Date(e.date).getDate())
},
yAxis: {
type: 'value'
},
series: [
{
data: data.map((e) => e.total),
type: 'line',
smooth: true
}
]
};
function echartsinit(node, option) {
const chart = charts.init(node);
chart.setOption(option);
}
</script>
<div class="container" use:echartsinit={option} />
<style>
.container {
width: 500px;
height: 500px;
}
</style>

31
src/lib/workers/worker.js Normal file
View File

@ -0,0 +1,31 @@
import { CronJob } from 'cron';
const job = new CronJob(
'0,31 * * * *', // cronTime
async function () {
notify();
}, // onTick
null, // onComplete
true, // start
'America/Detroit' // timeZone
);
async function notify() {
const res = await fetch("/api/budget");
const budgets = await res.json();
let text= '';
if (budgets.length > 0) {
text += `${budgets[0].name} - $${budgets[0].amount}\r\n`;
}
const notification = new Notification("Budgets",
{
body: text,
icon: "favicon.ico",
tag: "budget_update",
silent: false,
vibrate: [200, 100, 200, 100, 200, 100, 200]
});
}

View File

@ -1,7 +1,24 @@
<script> <script>
import TransactionList from '$lib/transactionList.svelte'; import TransactionList from '$lib/transactionList.svelte';
import Budgetlist from '$lib/budgetlist.svelte'; import Budgetlist from '$lib/budgetlist.svelte';
import { echarts } from '$lib/echarts'; import { browser } from '$app/environment';
import Waterfall from '$lib/waterfall.svelte';
let worker = $state();
$effect(async () => {
Notification.requestPermission().then((result) => {
console.log(result);
});
if (browser) {
// Check if we are in a browser
if (window.Worker) {
const MyWorker = await import('$workers/worker.js?worker');
// And initiate the
worker = new MyWorker.default();
} else {
console.warn('Web Workers are not supported in this browser.');
}
}
});
let { data } = $props(); let { data } = $props();
let total = $derived(data.total); let total = $derived(data.total);
@ -9,7 +26,7 @@
let underAllocatedTrans = $derived(data.underAllocatedTrans); let underAllocatedTrans = $derived(data.underAllocatedTrans);
let budgets = $derived(data.budgets); let budgets = $derived(data.budgets);
let budgetTransactions = $derived(data.budgetTransactions); let budgetTransactions = $derived(data.budgetTransactions);
let last30days = $derived(data.last30DaysTransactionsSums.reverse()); let last30days = $derived(data.last30DaysTransactionsSums);
let orphanedTransactions = $derived(data.orphanedTransactions); let orphanedTransactions = $derived(data.orphanedTransactions);
</script> </script>
@ -38,3 +55,5 @@
<div class="text-xl divider">Orphans</div> <div class="text-xl divider">Orphans</div>
<Budgetlist transactions={orphanedTransactions} /> <Budgetlist transactions={orphanedTransactions} />
<Waterfall {last30days} {budgets} />

View File

@ -1,5 +1,5 @@
import { json } from '@sveltejs/kit'; import { json } from '@sveltejs/kit';
import { addBudget, deleteBudget } from '$lib/db'; import { addBudget, deleteBudget, getBudgets } from '$lib/db';
// In-memory store for demonstration (replace with a database in production) // In-memory store for demonstration (replace with a database in production)
let budgets = []; let budgets = [];
@ -16,3 +16,13 @@ export async function POST({ request }) {
return json(res, { status: 201 }); return json(res, { status: 201 });
} }
export async function GET() {
// Fetch all budgets
try {
let res = await getBudgets();
return json(res, { status: 200 });
} catch (error) {
return json({ error: 'Failed to fetch budgets' }, { status: 500 });
}
}

View File

@ -1,13 +1,20 @@
import adapter from '@sveltejs/adapter-node'; import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
kit: { kit: {
alias: {
$workers: 'src/lib/workers',
$config: 'src/config.js',
},
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter. // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters. // See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter() adapter: adapter()
} },
preprocess: vitePreprocess(),
}; };
export default config; export default config;

BIN
united-login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB