imprnt-plugin-kopeika 0.1.0 → 0.1.1-edge.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/check.js +21 -8
- package/kopeika.js +62 -14
- package/package.json +2 -2
package/check.js
CHANGED
|
@@ -12,16 +12,29 @@ var problems = [];
|
|
|
12
12
|
var notes = [];
|
|
13
13
|
function git(args) {
|
|
14
14
|
const r = spawnSync("git", args, { cwd: ROOT, encoding: "utf8" });
|
|
15
|
-
return r.status === 0 ? (r.stdout ?? "").trim() :
|
|
15
|
+
return r.status === 0 ? (r.stdout ?? "").trim() : null;
|
|
16
16
|
}
|
|
17
|
-
var
|
|
18
|
-
var
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
notes.push("data: committed (no remote — canonical local store, safe)");
|
|
17
|
+
var dataLs = git(["ls-files", "data"]);
|
|
18
|
+
var deployLs = git(["ls-files", "deploy"]);
|
|
19
|
+
var remoteLs = git(["remote"]);
|
|
20
|
+
if (dataLs === null || deployLs === null || remoteLs === null) {
|
|
21
|
+
notes.push("data: could not check git tracking (git unavailable or not a repo)");
|
|
23
22
|
} else {
|
|
24
|
-
|
|
23
|
+
const dataTracked = dataLs.length > 0;
|
|
24
|
+
const deployTracked = deployLs.length > 0;
|
|
25
|
+
const hasRemote = remoteLs.length > 0;
|
|
26
|
+
if (dataTracked && hasRemote) {
|
|
27
|
+
problems.push("data/ is git-tracked in a repo that HAS A REMOTE — your financial data (ledger, raw bank " + "exports, profile.json) could be pushed. Add data/ to .gitignore, or remove the remote. " + "Committing data/ is only safe in a remoteless local store like the imprnt vault.");
|
|
28
|
+
} else if (dataTracked) {
|
|
29
|
+
notes.push("data: committed (no remote — canonical local store, safe)");
|
|
30
|
+
} else {
|
|
31
|
+
notes.push("data: not tracked by git");
|
|
32
|
+
}
|
|
33
|
+
if (deployTracked && hasRemote) {
|
|
34
|
+
problems.push("deploy/ is git-tracked in a repo that HAS A REMOTE — the rendered dashboard (net worth, " + "savings, transactions) could be pushed. Add deploy/ to .gitignore, or remove the remote.");
|
|
35
|
+
} else if (deployTracked) {
|
|
36
|
+
notes.push("deploy: committed (no remote — safe)");
|
|
37
|
+
}
|
|
25
38
|
}
|
|
26
39
|
var profilePath = join(DATA, "profile.json");
|
|
27
40
|
if (existsSync(profilePath)) {
|
package/kopeika.js
CHANGED
|
@@ -1017,9 +1017,6 @@ function renderDashboard(input) {
|
|
|
1017
1017
|
<meta charset="utf-8" />
|
|
1018
1018
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
|
|
1019
1019
|
<title>${esc(t("title"))} · ${esc(prettyMonth(focus.month))}</title>
|
|
1020
|
-
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
1021
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
1022
|
-
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Space+Grotesk:wght@500;600;700&family=JetBrains+Mono:wght@500;600;700&display=swap" rel="stylesheet" />
|
|
1023
1020
|
<script>${themeBoot}</script>
|
|
1024
1021
|
<style>
|
|
1025
1022
|
${buildCss()}
|
|
@@ -1329,6 +1326,10 @@ function mapType(revolutType) {
|
|
|
1329
1326
|
function datePart(timestamp) {
|
|
1330
1327
|
return timestamp.trim().split(" ")[0] ?? "";
|
|
1331
1328
|
}
|
|
1329
|
+
function timePart(timestamp) {
|
|
1330
|
+
const t2 = timestamp.trim().split(" ")[1] ?? "";
|
|
1331
|
+
return /^\d{2}:\d{2}:\d{2}$/.test(t2) ? t2 : "";
|
|
1332
|
+
}
|
|
1332
1333
|
function parseRevolut(text) {
|
|
1333
1334
|
const { header, records } = parseCsv(text);
|
|
1334
1335
|
for (const required of REQUIRED_HEADERS) {
|
|
@@ -1379,7 +1380,8 @@ function parseRevolut(text) {
|
|
|
1379
1380
|
note: "",
|
|
1380
1381
|
transferCandidate,
|
|
1381
1382
|
amountEur: null,
|
|
1382
|
-
balance
|
|
1383
|
+
balance,
|
|
1384
|
+
dedupExtra: timePart(rec.get("Started Date"))
|
|
1383
1385
|
});
|
|
1384
1386
|
}
|
|
1385
1387
|
return rows;
|
|
@@ -1548,7 +1550,7 @@ function toIsoDate(raw) {
|
|
|
1548
1550
|
return "";
|
|
1549
1551
|
return `${yyyy}-${mm}-${dd}`;
|
|
1550
1552
|
}
|
|
1551
|
-
function
|
|
1553
|
+
function timePart2(raw) {
|
|
1552
1554
|
const t2 = raw.trim().slice(11);
|
|
1553
1555
|
return /^\d{2}:\d{2}:\d{2}$/.test(t2) ? t2 : "";
|
|
1554
1556
|
}
|
|
@@ -1603,7 +1605,7 @@ function parseTbank(text) {
|
|
|
1603
1605
|
transferCandidate: false,
|
|
1604
1606
|
amountEur: currency === "EUR" ? amount : null,
|
|
1605
1607
|
balance: null,
|
|
1606
|
-
dedupExtra:
|
|
1608
|
+
dedupExtra: timePart2(opTimestamp)
|
|
1607
1609
|
});
|
|
1608
1610
|
}
|
|
1609
1611
|
return rows;
|
|
@@ -1629,6 +1631,10 @@ function toIsoDate2(raw) {
|
|
|
1629
1631
|
return "";
|
|
1630
1632
|
return `${yyyy}-${mm}-${dd}`;
|
|
1631
1633
|
}
|
|
1634
|
+
function timePart3(raw) {
|
|
1635
|
+
const t2 = raw.trim().slice(11);
|
|
1636
|
+
return /^\d{2}:\d{2}:\d{2}$/.test(t2) ? t2 : "";
|
|
1637
|
+
}
|
|
1632
1638
|
function parseMagnitude(raw) {
|
|
1633
1639
|
return Number(raw.replace(/\s/g, "").replace(",", "."));
|
|
1634
1640
|
}
|
|
@@ -1645,7 +1651,8 @@ function parseAlfa(text) {
|
|
|
1645
1651
|
const accountName = rec.get("accountName").trim();
|
|
1646
1652
|
if (!SAVINGS_ACCOUNT.test(accountName))
|
|
1647
1653
|
continue;
|
|
1648
|
-
const
|
|
1654
|
+
const opTimestamp = rec.get("operationDate");
|
|
1655
|
+
const date = toIsoDate2(opTimestamp);
|
|
1649
1656
|
if (date === "")
|
|
1650
1657
|
continue;
|
|
1651
1658
|
const magnitude = parseMagnitude(rec.get("amount").trim());
|
|
@@ -1672,7 +1679,8 @@ function parseAlfa(text) {
|
|
|
1672
1679
|
note: category,
|
|
1673
1680
|
transferCandidate: type === "transfer",
|
|
1674
1681
|
amountEur: currency === "EUR" ? amount : null,
|
|
1675
|
-
balance: null
|
|
1682
|
+
balance: null,
|
|
1683
|
+
dedupExtra: timePart3(opTimestamp)
|
|
1676
1684
|
});
|
|
1677
1685
|
}
|
|
1678
1686
|
return rows;
|
|
@@ -1734,6 +1742,19 @@ function toEur(amount_native, currency, isoDate, table) {
|
|
|
1734
1742
|
}
|
|
1735
1743
|
return { amount_eur: round22(amount_native * rate), missing: null };
|
|
1736
1744
|
}
|
|
1745
|
+
function backfillEur(txs, table) {
|
|
1746
|
+
let filled = 0;
|
|
1747
|
+
for (const tx of txs) {
|
|
1748
|
+
if (tx.amount_eur !== null)
|
|
1749
|
+
continue;
|
|
1750
|
+
const fx = toEur(tx.amount_native, tx.currency, tx.date, table);
|
|
1751
|
+
if (fx.amount_eur !== null) {
|
|
1752
|
+
tx.amount_eur = fx.amount_eur;
|
|
1753
|
+
filled += 1;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
return filled;
|
|
1757
|
+
}
|
|
1737
1758
|
function rateToEur(table, currency, isoDate) {
|
|
1738
1759
|
const cur = currency.toUpperCase();
|
|
1739
1760
|
if (cur === "EUR")
|
|
@@ -1977,8 +1998,9 @@ function transactionId(input) {
|
|
|
1977
1998
|
}
|
|
1978
1999
|
|
|
1979
2000
|
// src/ledger.ts
|
|
1980
|
-
import {
|
|
1981
|
-
import {
|
|
2001
|
+
import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync5, writeFileSync } from "node:fs";
|
|
2002
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
2003
|
+
import { basename, dirname, extname, join } from "node:path";
|
|
1982
2004
|
|
|
1983
2005
|
// src/types.ts
|
|
1984
2006
|
var TX_TYPES = [
|
|
@@ -2114,10 +2136,24 @@ function appendDeduped(existing, candidates) {
|
|
|
2114
2136
|
return { appended, skippedDuplicate, merged };
|
|
2115
2137
|
}
|
|
2116
2138
|
function archiveRaw(dataDir, source, sourceFilePath) {
|
|
2117
|
-
const name = basename(sourceFilePath);
|
|
2118
2139
|
const destDir = join(dataDir, "raw", source);
|
|
2119
2140
|
mkdirSync(destDir, { recursive: true });
|
|
2120
|
-
|
|
2141
|
+
const srcBytes = readFileSync5(sourceFilePath);
|
|
2142
|
+
let name = basename(sourceFilePath);
|
|
2143
|
+
if (existsSync5(join(destDir, name)) && !srcBytes.equals(readFileSync5(join(destDir, name)))) {
|
|
2144
|
+
const ext = extname(name);
|
|
2145
|
+
const stem = name.slice(0, name.length - ext.length);
|
|
2146
|
+
const hash8 = createHash2("sha256").update(srcBytes).digest("hex").slice(0, 8);
|
|
2147
|
+
name = `${stem}-${hash8}${ext}`;
|
|
2148
|
+
let n = 2;
|
|
2149
|
+
while (existsSync5(join(destDir, name)) && !srcBytes.equals(readFileSync5(join(destDir, name)))) {
|
|
2150
|
+
name = `${stem}-${hash8}-${n}${ext}`;
|
|
2151
|
+
n += 1;
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
const dest = join(destDir, name);
|
|
2155
|
+
if (!existsSync5(dest))
|
|
2156
|
+
writeFileSync(dest, srcBytes);
|
|
2121
2157
|
return name;
|
|
2122
2158
|
}
|
|
2123
2159
|
|
|
@@ -2492,14 +2528,19 @@ async function cmdImport(args) {
|
|
|
2492
2528
|
});
|
|
2493
2529
|
const existing = loadLedger(LEDGER_PATH);
|
|
2494
2530
|
const { appended, skippedDuplicate, merged } = appendDeduped(existing, candidates);
|
|
2531
|
+
const backfilled = backfillEur(merged, rates);
|
|
2495
2532
|
writeLedger(LEDGER_PATH, merged);
|
|
2496
2533
|
console.log(`imported ${appended} / skipped-dup ${skippedDuplicate} / skipped-non-completed ${skippedNonCompleted}`);
|
|
2534
|
+
if (backfilled > 0) {
|
|
2535
|
+
console.log(`backfilled amount_eur for ${backfilled} earlier row(s) from data/rates.csv`);
|
|
2536
|
+
}
|
|
2497
2537
|
if (missingRates.size > 0) {
|
|
2498
2538
|
console.log("");
|
|
2499
2539
|
console.log(`⚠ missing FX rates for ${missingRates.size} (month, currency) pair(s) — amount_eur left empty:`);
|
|
2500
2540
|
for (const { month, currency } of [...missingRates.values()].sort((a, b) => `${a.month}${a.currency}`.localeCompare(`${b.month}${b.currency}`))) {
|
|
2501
2541
|
console.log(` ${month} ${currency} (add a row to data/rates.csv: ${month},${currency},<rate_to_eur>)`);
|
|
2502
2542
|
}
|
|
2543
|
+
console.log(" then re-run `kopeika report` — already-imported rows are backfilled once the rate exists.");
|
|
2503
2544
|
}
|
|
2504
2545
|
return 0;
|
|
2505
2546
|
}
|
|
@@ -2725,6 +2766,12 @@ async function cmdReport(args) {
|
|
|
2725
2766
|
console.log("ledger is empty — import something first.");
|
|
2726
2767
|
return 0;
|
|
2727
2768
|
}
|
|
2769
|
+
const backfilled = backfillEur(ledger, loadRates(RATES_PATH));
|
|
2770
|
+
if (backfilled > 0) {
|
|
2771
|
+
writeLedger(LEDGER_PATH, ledger);
|
|
2772
|
+
console.log(`backfilled amount_eur for ${backfilled} row(s) from data/rates.csv`);
|
|
2773
|
+
console.log("");
|
|
2774
|
+
}
|
|
2728
2775
|
const monthFlag = flagString(args, "month");
|
|
2729
2776
|
const fromFlag = flagString(args, "from");
|
|
2730
2777
|
const htmlPath = flagString(args, "html");
|
|
@@ -2799,10 +2846,11 @@ async function cmdReport(args) {
|
|
|
2799
2846
|
}
|
|
2800
2847
|
const now = new Date;
|
|
2801
2848
|
const nowMonth = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
2802
|
-
const spendMonths = report.months.map((m) => m.month)
|
|
2849
|
+
const spendMonths = report.months.map((m) => m.month);
|
|
2803
2850
|
const months = spendMonths.map((m) => ({ month: m, groups: buildSpendGroups(ledger, m, tiers) }));
|
|
2804
2851
|
if (spendMonths.length > 0) {
|
|
2805
|
-
|
|
2852
|
+
const latestYear = spendMonths[spendMonths.length - 1].slice(0, 4);
|
|
2853
|
+
months.push({ month: latestYear, groups: buildSpendGroups(ledger, latestYear, tiers) });
|
|
2806
2854
|
}
|
|
2807
2855
|
const selectedMonth = spendMonths.length > 0 ? spendMonths[spendMonths.length - 1] : focus.month;
|
|
2808
2856
|
const lang = flagString(args, "lang") === "ru" ? "ru" : "en";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imprnt-plugin-kopeika",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1-edge.62",
|
|
4
4
|
"description": "imprnt plugin: a deterministic local-first personal-finance and net-worth CLI. Parses bank CSV exports into one clean ledger and renders a self-contained dashboard. All personal data stays in its own gitignored folder; no LLM at runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"check.js",
|
|
10
10
|
"kopeika.js",
|
|
11
11
|
"profile.example.json",
|
|
12
|
-
"proposed",
|
|
12
|
+
"proposed/.gitkeep",
|
|
13
13
|
"docs"
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|