40 Commits

Author SHA1 Message Date
rlkollman 850c0d7a6a Add budget.js
Create and publish Docker image on GHCR / publish-docker-image (push) Has been cancelled
Get latest Actual release / get-version (push) Has been cancelled
2026-05-24 09:12:52 -05:00
rlkollman 30be20eac6 Add update-investments.js
Create and publish Docker image on GHCR / publish-docker-image (push) Has been cancelled
2026-05-24 09:11:52 -05:00
rlkollman 6ed45c4785 Added apache and php to installer for support scripts
Create and publish Docker image on GHCR / publish-docker-image (push) Has been cancelled
2026-05-24 09:05:24 -05:00
rlkollman 77b7333e81 Add install.sh
Create and publish Docker image on GHCR / publish-docker-image (push) Has been cancelled
2026-05-24 09:00:52 -05:00
rlkollman 7899b45e9f Update zestimate to use puppeteer instead of selenium
Create and publish Docker image on GHCR / publish-docker-image (push) Has been cancelled
2026-05-24 09:00:21 -05:00
Robert Dyer 526cb5fa3a support simple daily interest 2026-05-21 17:07:17 -05:00
Robert Dyer f967435881 avoid daylight savings time rounding issue on number of days 2026-05-21 16:55:36 -05:00
Robert Dyer 867694ce9a fix issue with zestimate sometimes returning 0 2026-05-21 15:27:24 -05:00
Robert Dyer c653f8c28b bump module versions 2026-05-21 15:25:05 -05:00
Robert Dyer f02b397fbb New Actual release version 2026-05-18 23:57:55 +00:00
Robert Dyer 72ba2dcb4f New Actual release version 2026-05-17 23:51:56 +00:00
Robert Dyer db12151cc3 New Actual release version 2026-05-08 23:48:52 +00:00
Robert Dyer 511a5a537f New Actual release version 2026-05-03 23:36:14 +00:00
Robert Dyer aa02e0ef5b New Actual release version 2026-04-05 23:26:01 +00:00
Robert Dyer 44eb9645b4 New Actual release version 2026-03-03 23:18:43 +00:00
Robert Dyer a5c9c0fd6f add extra logging to simplefin setup 2026-02-28 12:18:31 -06:00
Robert Dyer f18c6d1e1c New Actual release version 2026-02-22 23:17:57 +00:00
Robert Dyer f3b115603d New Actual release version 2026-02-02 23:20:27 +00:00
Robert Dyer 6ebcc1cc7a update the field being pulled for zestimates 2026-02-01 12:36:01 -06:00
sventec a4c85a76d5 docs: update INVESTMENT_PAYEE_NAME comment (#41)
Co-authored-by: sventec <18218761+sventec@users.noreply.github.com>
2026-01-06 08:51:53 -06:00
Robert Dyer 543bb71056 New Actual release version 2026-01-04 23:12:59 +00:00
Robert Dyer eb46d2cc72 New Actual release version 2025-12-03 23:10:51 +00:00
Robert Dyer d878f77fc0 New Actual release version 2025-11-25 23:12:13 +00:00
Robert Dyer 4a24330dea New Actual release version 2025-11-24 23:12:15 +00:00
Robert Dyer 2c6e37ffc6 add missing ENV to Dockerfile 2025-11-18 08:18:59 -06:00
Jacob Schooley b2a45903f9 Add sync-crypto.js for other cryptocurrencies besides just BTC (#31)
* add sync-crypto.js

* update crypto payee name
2025-11-17 09:44:16 -06:00
Robert Dyer c031e5bffd feat: Build multi-architecture Docker images (#27)
* feat: Build multi-architecture Docker images

Updates the GitHub Actions workflow to build Docker images for both amd64 and arm64 architectures.

This is achieved by:
- Adding a QEMU setup step (`docker/setup-qemu-action@v3`).
- Adding a Docker Buildx setup step.
- Specifying `linux/amd64,linux/arm64` in the `platforms` input of the `docker/build-push-action@v6` step.

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2025-11-17 08:38:10 -06:00
Robert Dyer 066b217347 add more items to Docker ignore file - fixes #37 2025-11-17 08:25:43 -06:00
Robert Dyer 95d4c80b62 New Actual release version 2025-11-04 23:11:45 +00:00
Robert Dyer 18e58a5966 New Actual release version 2025-10-02 23:10:43 +00:00
Robert Dyer 84ea8d2e76 fixes #32 - add docs to explain how to configure when using OIDC 2025-09-08 09:53:24 -05:00
Robert Dyer 9d82f204f9 New Actual release version 2025-09-04 23:10:56 +00:00
Robert Dyer db2b47b180 New Actual release version 2025-08-02 23:13:20 +00:00
Robert Dyer f56b811d64 New Actual release version 2025-07-18 23:13:04 +00:00
Robert Dyer 1217bce02d New Actual release version 2025-07-15 23:12:18 +00:00
Robert Dyer 0e376c02f4 New Actual release version 2025-07-03 23:12:41 +00:00
Robert Dyer 0ce92abd16 New Actual release version 2025-07-01 23:12:10 +00:00
Robert Dyer 6b6df500ee bump versions 2025-06-28 00:26:06 -05:00
Robert Dyer 4dc30e44ff bump dependency 2025-06-28 00:21:31 -05:00
Robert Dyer 3792d96ca0 New Actual release version 2025-06-04 23:12:17 +00:00
15 changed files with 1639 additions and 289 deletions
+5
View File
@@ -1,2 +1,7 @@
.git/
.idea/
.vscode/
.github/
.gitignore
node_modules/
package-lock.json
+7
View File
@@ -43,6 +43,12 @@ jobs:
# set latest tag for default branch
type=raw,value=latest,enable={{is_default_branch}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push the Docker image
id: push
uses: docker/build-push-action@v6
@@ -52,3 +58,4 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
+12 -2
View File
@@ -49,9 +49,9 @@ ENV ACTUAL_CACHE_DIR="./cache"
# optional, name of the payee for added interest transactions
ENV INTEREST_PAYEE_NAME="Loan Interest"
# optional, name of the payee for added interest transactions
# optional, name of the payee for added investment transactions
ENV INVESTMENT_PAYEE_NAME="Investment"
# optional, name of the cateogry group for added investment tracking transactions
# optional, name of the category group for added investment tracking transactions
ENV INVESTMENT_CATEGORY_GROUP_NAME="Income"
# optional, name of the category for added investment tracking transactions
ENV INVESTMENT_CATEGORY_NAME="Investment"
@@ -59,11 +59,21 @@ ENV INVESTMENT_CATEGORY_NAME="Investment"
# optional, for logging into SimpleFIN
ENV SIMPLEFIN_CREDENTIALS=""
# optional, name of the payee for Zestimate entries
ENV ZESTIMATE_PAYEE_NAME="Zestimate"
# optional, name of the payee for KBB entries
ENV KBB_PAYEE_NAME="KBB"
# optional, for retrieving Bitcoin Price (these default to Kraken USD)
ENV BITCOIN_PRICE_URL="https://api.kraken.com/0/public/Ticker?pair=xbtusd"
ENV BITCOIN_PRICE_JSON_PATH="result.XXBTZUSD.c[0]"
ENV BITCOIN_PAYEE_NAME="Bitcoin Price Change"
#optional, RentCast API key for fetching property data
ENV RENTCAST_API_KEY=""
ENV RENTCAST_PAYEE_NAME="RentCast"
VOLUME ./cache
# Copy the current directory contents into the container at /usr/src/app
+61 -2
View File
@@ -47,7 +47,7 @@ ACTUAL_CACHE_DIR="./cache"
# optional, name of the payee for added interest transactions
INTEREST_PAYEE_NAME="Loan Interest"
# optional, name of the payee for added interest transactions
# optional, name of the payee for added investment transactions
INVESTMENT_PAYEE_NAME="Investment"
# optional, name of the category group for added investment tracking transactions
INVESTMENT_CATEGORY_GROUP_NAME="Income"
@@ -72,6 +72,23 @@ RENTCAST_API_KEY="<Rentcast API key>"
RENTCAST_PAYEE_NAME="RentCast"
```
### OIDC Auth Provider Support
When using OIDC, in your Actual Budget server config you must be able to log in
with a password.
Set the following in your **server config** (not in your helpers config!), then
on initial login you must set a password:
```
ACTUAL_OPENID_AUTH_METHOD=openid
ACTUAL_LOGIN_METHOD=openid
ACTUAL_ALLOWED_LOGIN_METHODS=openid,password,header
ACTUAL_OPENID_ENFORCE=false
```
Then configure the helpers for password login, as shown above.
## Installation
Run `npm install` to install any required dependencies.
@@ -168,7 +185,9 @@ interest transaction on the 28th of the month, set the account note to
By default, interest is calculated using the 30/360 method where interest is
computed monthly using 30/360 (or 1/12) of the interest rate. If you need to
compute interest using the ACTUAL/ACTUAL method, set `interest:actual` in the
note. If you need to compute interest daily, set `interest:daily`.
note. If you need to compute interest daily, set `interest:daily`. For most
student loans, you probably want simple dailiy interest
`interest:daily-simple`.
You can optionally change the payee used for the interest transactions by
setting `INTEREST_PAYEE_NAME` in the `.env` file.
@@ -390,3 +409,43 @@ node sync-bitcoin.js
```
It is recommended to run this script once per day or week.
### Tracking Crypto Prices
This script tracks the value of any cryptocurrency. Similar to the Bitcoin
script, it adds new transactions to keep the account balance equal to the
latest value. Values are retrieved from the Kraken API, but the script is
customizable to use other APIs if needed.
To use, set these tags in the account notes:
- `CRYPTO:<symbol>,<symbol>,...` - where each symbol is a cryptocurrency
you want to track, e.g. `CRYPTO:BTC,ETH,DOGE`
- `<symbol>:<amount>[,<krakenPair>]` - where `<symbol>` is the cryptocurrency
symbol and `<amount>` is the amount you own. If using Kraken, specify the Kraken
pair to use for the price. Obtain this by visiting
[https://api.kraken.com/0/public/Ticker?pair=<symbol\>usd](https://api.kraken.com/0/public/Ticker?pair=btcusd)
and locating the key under `result` that matches your symbol.
For example, if you own 0.01 BTC and 1 ETH, your account note would look like:
```
CRYPTO:BTC,ETH
BTC:0.01,XXBTZUSD
ETH:1,XXETHZUSD
```
To use a different API, set the following in your `.env` file: (example using Bitcoin)
```shell
CRYPTO_PRICE_URL_BTC="https://api.kraken.com/0/public/Ticker?pair=xbtgbp"
CRYPTO_PRICE_JSON_PATH_BTC="result.XXBTZGBP.c[0]"
```
To run:
```console
node sync-crypto.js
```
It is recommended to run this script once per day or week. If you are retrieving
BTC prices using this script, DO NOT run the `sync-bitcoin.js` script as well, as it will
set the account balance to the value of your BTC holdings only.
+1 -1
View File
@@ -1 +1 @@
v25.5.0
v26.5.2
+10 -2
View File
@@ -41,12 +41,15 @@ function daysInYear(year) {
const lastDate = await getLastTransactionDate(account, cutoff);
if (!lastDate) continue;
const daysPassed = Math.floor((interestTransactionDate - new Date(lastDate)) / 86400000);
const daysPassed = Math.round(
(interestTransactionDate.setHours(0, 0, 0, 0) - new Date(lastDate).setHours(0, 0, 0, 0)) / 86400000
);
let period = 12;
let numPeriods = 1
switch (kind) {
case 'daily':
case 'daily-simple':
period = daysInYear(interestTransactionDate.getFullYear());
numPeriods = daysPassed;
break;
@@ -58,7 +61,12 @@ function daysInYear(year) {
}
const balance = await getAccountBalance(account, interestTransactionDate);
const compoundedInterest = Math.round(balance * (Math.pow(1 + interestRate / period, numPeriods) - 1));
let compoundedInterest;
if (kind == 'daily-simple')
compoundedInterest = Math.round(balance * (interestRate / 365) * numPeriods);
else
compoundedInterest = Math.round(balance * (Math.pow(1 + interestRate / period, numPeriods) - 1));
interestRate = showPercent(interestRate);
+74
View File
@@ -0,0 +1,74 @@
const api = require('@actual-app/api');
const { closeBudget, ensurePayee, getAccountBalance, getAccountNote, getTagValue, openBudget, showPercent, sleep,
getBudgetMonth, getCategoryGroups, getCategories } = require('./utils');
require("dotenv").config();
(async function () {
await openBudget();
var today = new Date();
var yyyy = today.getFullYear();
var mm = (today.getMonth() + 1).toString().padStart(2, '0');
var budgetMonth = yyyy + '-' + mm;
const budget = await api.getBudgetMonth(budgetMonth);
console.log('');
console.log('~~!!'); // Make it easy to parse for e-mail
console.log('Budget for Month: ' + budgetMonth);
console.log('Total Budget: $' + (budget.totalBudgeted * .01).toFixed(2).padStart(8, ' '));
console.log('Income: $' + (budget.totalIncome * .01).toFixed(2).padStart(8, ' '));
console.log('Spent: $' + (budget.totalSpent * .01).toFixed(2).padStart(8, ' '));
console.log('Balance: $' + (budget.totalBalance * .01).toFixed(2).padStart(8, ' '));
console.log('----------------------------------------------------');
const categoryGroups = await api.getCategoryGroups();
for (const categoryGroup of budget.categoryGroups) {
if (categoryGroup.hidden || categoryGroup.budgeted == 0 || categoryGroup.is_income) {
continue;
}
const categorySpentPercent = ((categoryGroup.spent * .01) / (categoryGroup.budgeted * .01) * -100);
const categoryBalancePercent = ((categoryGroup.balance * .01) / (categoryGroup.budgeted * .01) * 10);
console.log(categoryGroup.name.padEnd(21, ' ') + ' ' + categorySpentPercent.toFixed(1).trim().padStart(7, ' ') + '% spent of $' + (categoryGroup.budgeted * .01).toFixed(2).trim().padStart(8, ' '));
}
console.log('!!~~'); // Make it easy to parse for e-mail
console.log('');
var transactionDate = new Date();
transactionDate.setDate(transactionDate.getDate() - 1);
let txDate = transactionDate.toISOString().substring(0, 10);
console.log('Transactions on ' + txDate);
console.log('DATE ACCOUNT PAYEE NOTES / MEMO AMOUNT');
console.log('-------------------------------------------------------------------------------------------------------------------------');
const payees = await api.getPayees();
const accounts = await api.getAccounts();
( await api.runQuery(
api.q('transactions')
.filter({
date: { $gte: txDate },
})
.select('*')
)
).data.map(row => {
let payeeName = payees.find(p => p.id === row.payee)?.name;
let accountName = accounts.find(a => a.id === row.account)?.name;
console.log(row.date + ' ' +
fixedStringLength(accountName, 20) + ' ' +
fixedStringLength(payeeName, 25) + ' ' +
fixedStringLength(row.notes, 45) + ' ' +
('$' + (row.amount / 100).toFixed(2).trim()).padStart(11, ' '));
});
await closeBudget();
})();
function fixedStringLength(str, length) {
if (str === null) {
str = '';
}
return str.padEnd(length).substring(0, length);
}
+1 -1
View File
@@ -13,7 +13,7 @@ ACTUAL_CACHE_DIR="./cache"
# optional, name of the payee for added interest transactions
INTEREST_PAYEE_NAME="Loan Interest"
# optional, name of the payee for added interest transactions
# optional, name of the payee for added investment transactions
INVESTMENT_PAYEE_NAME="Investment"
# optional, name of the category group for added investment tracking transactions
INVESTMENT_CATEGORY_GROUP_NAME="Income"
+51
View File
@@ -0,0 +1,51 @@
# Require APT to utilizelocal proxy
echo "Acquire::http::proxy \"http://192.168.128.185:3142\";" >> /etc/apt/apt.conf.d/02proxy
# Set TimeZone :)
timedatectl set-timezone America/Chicago
# Perform update / upgrade of all existing packages and repos
apt update && apt upgrade -y
# Install curl and git if not present
apt install curl git ca-certificates fonts-liberation libasound2 libatk-bridge2.0-0 libatk1.0-0 \
libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libglib2.0-0 \
libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 \
libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 \
libxrandr2 libxrender1 libxss1 libxtst6 lsb-release wget xdg-utils \
apache2 php php-curl php-json php-cgi libapache2-mod-php -y
# Add NodeSource repository (replace 22.x with your desired version, e.g., 20.x, 18.x)
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
# Install Node.js
apt install -y nodejs
# Git clone the actual-helpers repo
cd /opt
git clone https://git.kollman.net/rlkollman/actual-helpers
# Install all dependencies
cd /opt/actual-helpers
npm install
# Write crontab file with scheduled tasks
echo "" >> /etc/crontab
echo "# Sync ActualBudget Bank Accounts daily at 2 AM" >> /etc/crontab
echo "00 2 * * * root cd /opt/actual-helpers && node sync-banks.js > /dev/null" >> /etc/crontab
echo "05 2 * * * root cd /opt/actual-helpers && node update-investments.js > /dev/null" >> /etc/crontab
echo "" >> /etc/crontab
echo "# Sync ActualBudget Vehicles & Apply Mortgage Interest on the 1st day of every month at 3 AM" >> /etc/crontab
echo "00 3 1 * * root cd /opt/actual-helpers && node kbb.js > /dev/null" >> /etc/crontab
echo "15 3 6 * * root cd /opt/actual-helpers && node apply-interest.js > /dev/null" >> /etc/crontab
echo "" >> /etc/crontab
echo "# Sync ActualBudget Residence through RentCast on the 1st & 15th day of every month at 4 AM" >> /etc/crontab
echo "#00 4 1 * * root cd /opt/actual-helpers && node rentcast.js > /dev/null" >> /etc/crontab
echo "#00 4 15 * * root cd /opt/actual-helpers && node rentcast.js > /dev/null" >> /etc/crontab
echo "" >> /etc/crontab
echo "# Sync ActualBudget Residence through Zillow on the 1st & 16th day of every month at 5 AM" >> /etc/crontab
echo "30 4 1 * * root cd /opt/actual-helpers && node zestimate.js > /dev/null" >> /etc/crontab
echo "30 4 16 * * root cd /opt/actual-helpers && node zestimate.js > /dev/null" >> /etc/crontab
echo "" >> /etc/crontab
echo "# Output the current budget status to a file" >> /etc/crontab
echo "00 5 * * * root cd /opt/actual-helpers && node budget.js > /root/budget.txt" >> /etc/crontab
+1186 -249
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -1,7 +1,6 @@
{
"dependencies": {
"@actual-app/api": "latest",
"better-sqlite3": "^11.7.2",
"chromedriver": "^131.0.4",
"dotenv": "^16.4.5",
"jsdom": "^24.1.0",
+111
View File
@@ -0,0 +1,111 @@
const { closeBudget, openBudget, getTransactions, getAccountNote, getAccountBalance, getTagValue, ensurePayee } = require('./utils');
const api = require('@actual-app/api');
function getValueAtPath(obj, path) {
const keys = path.split('.').filter(Boolean);
return keys.reduce((acc, key) => {
const match = key.match(/^([^\[\]]+)(\[(\d+)\])?$/);
if (match) {
const property = match[1];
const index = match[3];
acc = acc[property];
if (index !== undefined) {
acc = acc[parseInt(index, 10)];
}
} else {
acc = acc[key];
}
return acc;
}, obj);
}
async function getCryptoPrice(crypto, krakenPath) {
const url = process.env[`CRYPTO_PRICE_URL_${crypto.toUpperCase()}`] || `https://api.kraken.com/0/public/Ticker?pair=${crypto}usd`;
if (!krakenPath && !process.env[`CRYPTO_PRICE_JSON_PATH_${crypto.toUpperCase()}`]) {
console.error(`No Kraken path provided for ${crypto}. Please set CRYPTO_PRICE_JSON_PATH_${crypto.toUpperCase()} environment variable or provide krakenPath argument.`);
return undefined;
}
const path = process.env[`CRYPTO_PRICE_JSON_PATH_${crypto.toUpperCase()}`] || `result.${krakenPath}.c[0]`;
try {
const response = await fetch(url);
const json = await response.json();
return getValueAtPath(json, path);
} catch (error) {
console.error(`Error fetching price for ${crypto}:`, error);
return undefined;
}
}
(async () => {
await openBudget();
const payeeId = await ensurePayee(process.env.CRYPTO_PAYEE_NAME || 'Crypto Price Change');
const accounts = await api.getAccounts();
for (const account of accounts) {
if (account.closed) {
continue;
}
// Need to set cutoff date to tomorrow to ensure we get the latest transactions
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() + 1);
const note = await getAccountNote(account, cutoffDate);
// Get cryptos list from the note
let cryptos = getTagValue(note, 'CRYPTO', null);
if (!cryptos) continue;
cryptos = cryptos.split(',').map(c => c.trim()).filter(c => c.length > 0);
if (cryptos.length === 0) continue;
console.log(`Processing account: ${account.name}, Cryptos: ${cryptos.join(', ')}`);
let totalTargetBalance = 0;
for (const crypto of cryptos) {
const cryptoTag = getTagValue(note, crypto, 0.0);
if (!cryptoTag) continue;
cryptoTagSplit = cryptoTag.split(',');
// Get the amount from the first part of the tag
amount = parseFloat(cryptoTagSplit[0]);
if (isNaN(amount) || amount <= 0) continue;
console.log(`Crypto: ${crypto}, Amount: ${amount}`);
// Get Kraken path from the second part of the tag, if it exists
let krakenPath = cryptoTagSplit[1]
console.log(`Kraken Path: ${krakenPath}`);
// Get the crypto price
const cryptoPrice = await getCryptoPrice(crypto, krakenPath);
if (!cryptoPrice) {
console.error(`Unable to retrieve price for ${crypto}. Check your CRYPTO_PRICE_URL_${crypto.toUpperCase()} and CRYPTO_PRICE_JSON_PATH_${crypto.toUpperCase()} environment variables`);
continue;
}
console.log(`Crypto: ${crypto}, Price: ${cryptoPrice}`);
const targetBalance = Math.round(cryptoPrice * amount * 100);
totalTargetBalance += targetBalance;
}
// Now we need to check if the total target balance is different from the current balance and update it
const currentBalance = await getAccountBalance(account, cutoffDate);
const diff = totalTargetBalance - currentBalance;
console.log(`Account: ${account.name}, Total Target Balance: ${totalTargetBalance}, Current Balance: ${currentBalance}, Diff: ${diff}`);
if (diff != 0) {
await api.importTransactions(account.id, [{
date: new Date(),
payee: payeeId,
amount: diff,
cleared: true,
reconciled: true,
notes: `Updated Crypto Price on ${new Date().toLocaleString()} (${cryptos.join(', ')})`,
}])
}
}
await closeBudget();
})();
+17 -9
View File
@@ -16,16 +16,21 @@ const getCredentials = async () => {
const response = await fetch(url, { method: 'post' });
const api_url = await response.text();
const rest = api_url.split('//', 2)[1];
const auth = rest.split('@', 1)[0];
const username = auth.split(':')[0];
const pw = auth.split(':')[1];
try {
const rest = api_url.split('//', 2)[1];
const auth = rest.split('@', 1)[0];
const username = auth.split(':')[0];
const pw = auth.split(':')[1];
const data = `${username}:${pw}`;
const cache = process.env.ACTUAL_CACHE_DIR || './cache';
fs.writeFileSync(cache + '/simplefin.credentials', data);
console.log('SimpleFIN credentials:', data);
return data;
const data = `${username}:${pw}`;
const cache = process.env.ACTUAL_CACHE_DIR || './cache';
fs.writeFileSync(cache + '/simplefin.credentials', data);
console.log('SimpleFIN credentials:', data);
return data;
} catch (err) {
console.log('Invalid SimpleFIN setup response:', api_url);
return undefined;
}
};
const loadCredentials = () => {
@@ -41,6 +46,9 @@ const getSimplefinBalances = async () => {
let credentials = loadCredentials();
if (!credentials) {
credentials = await getCredentials();
if (!credentials) {
return undefined;
}
}
const username = credentials.split(':')[0];
const pw = credentials.split(':')[1];
+61
View File
@@ -0,0 +1,61 @@
const api = require('@actual-app/api');
const fs = require('fs');
const readline = require('readline');
const { closeBudget, ensurePayee, getAccountBalance, getAccountNote, getTagValue, openBudget, showPercent, sleep,
getBudgetMonth, getCategoryGroups, getCategories } = require('./utils');
require("dotenv").config();
(async function () {
await openBudget();
const fileStream = fs.createReadStream("/var/www/html/data/investment-data.txt");
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
const payeeId = await ensurePayee('Change in Market Value');
for await (const line of rl) {
const parts = line.split('=');
const accountBalance = await api.getAccountBalance(parts[0]);
const adjustment = (accountBalance - parts[1].replace(/\./g, '')) * -1;
if (adjustment == 0) {
continue;
}
console.log(parts[0] + ' --> ' + parts[1] + ' --> $' + adjustment);
await api.importTransactions(parts[0], [{
date: new Date(),
payee: payeeId,
amount: adjustment,
cleared: true,
reconciled: true,
notes: `Change in Market Value`,
}]);
}
deleteFileIfExists('/var/www/html/data/investment-data.txt');
await closeBudget();
})();
function fixedStringLength(str, length) {
if (str === null) {
str = '';
}
return str.padEnd(length).substring(0, length);
}
async function deleteFileIfExists(filePath) {
try {
await fs.promises.unlink(filePath);
console.log(`File ${filePath} deleted successfully.`);
} catch (err) {
if (err.code === 'ENOENT') {
console.log(`File ${filePath} does not exist, so it was not deleted.`);
} else {
console.error('Error deleting file:', err.message);
}
}
}
+42 -22
View File
@@ -1,35 +1,55 @@
const { Builder, Browser, By, until } = require('selenium-webdriver')
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
const api = require('@actual-app/api');
const { closeBudget, ensurePayee, getAccountBalance, getAccountNote, getTagValue, openBudget, showPercent, sleep } = require('./utils');
require("dotenv").config();
puppeteer.use(StealthPlugin());
async function getZestimate(URL) {
let driver = await new Builder()
.forBrowser(Browser.CHROME)
.build();
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
try {
await driver.get(URL);
const html = await driver.wait(until.elementLocated(By.css('body')), 5000).getAttribute('innerHTML');
try {
let match = html.match(/"zestimate":"(\d+)"/);
if (match) {
return parseInt(match[1]) * 100;
await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 20000 });
const html = await page.content();
try {
console.log('Got html data!');
let match = html.match(/"price":"(\d+)"/);
if (match) {
console.log('matched 1st');
return parseInt(match[1]) * 100;
}
match = html.match(/\\"price\\":(\d+)/);
if (match) {
console.log('matched 2nd');
return parseInt(match[1]) * 100;
}
match = html.match(/\\"price\\":\\"(\d+)\\"/);
if (match) {
console.log('matched 3rd');
return parseInt(match[1]) * 100;
}
console.log('didn\'t match any :(');
console.log('~~!!');
// console.log(html);
console.log('!!~~');
} catch (error) {
console.log('Error parsing Zillow page:');
console.log(error);
console.log(html);
}
match = html.match(/\\"zestimate\\":\\"(\d+)\\"/);
if (match) {
return parseInt(match[1]) * 100;
}
} catch (error) {
console.log('Error parsing Zillow page:');
console.log(error);
console.log(html);
}
} catch (er) {
console.log('Error while fetching zestimate:');
console.log(er);
console.lo(html);
} finally {
await driver.quit();
await browser.close();
}
return undefined;
}