github John710/finapp v1.4.0-beta.6

latest releases: v1.4.0, v1.4.0-beta.8, v1.4.0-beta.7...
pre-release5 hours ago

[Unreleased]

Bug Fixes

  • Render calendar days in any timezone and parse grouped amounts by @John710
    formatDate passed 'YYYY-MM-DD' to new Date(), which reads it as UTC
    midnight, so every browser west of UTC showed due dates, transaction
    dates and next payments one day early. Date-only strings are now built
    from their parts at local midnight; ISO timestamps keep the Date parser.
    parseAmount replaced only the first comma, so "1,234.56" became 1.234.
    It now follows the backend CSV import rule: spaces, NBSP and apostrophes
    group digits, the last of ','/'.' is the decimal separator unless a lone
    separator repeats, and grouping has to come in threes; anything else is
    NaN instead of a silently truncated number.
    ReportsView formatted debts' due dates and recurring next dates with
    new Date(), and filtered upcoming payments by comparing that UTC instant
    with the current time, which dropped today's payments east of UTC. It
    now uses formatDate and compares the day strings.
  • Return DATE columns as YYYY-MM-DD text and harden the pool by @John710
    pg has no override for DATE (oid 1082), so it built a Date at local
    midnight of the Node process. With the container on Asia/Vladivostok,
    DATE '2026-10-29' reached clients as "2026-10-28T14:00:00.000Z"; the
    transaction edit form sliced that to 2026-10-28 and saving moved the
    transaction back a day. Every DATE column in every response (transactions,
    budgets, debts, recurring rules/exceptions/history) had the same shift,
    and budget edits sent the timestamp back where the schema expects a date.
    The db plugin now keeps DATE as the text PostgreSQL sends; TIMESTAMP is
    unchanged. The pool also logs idle-client errors instead of crashing the
    process on an unhandled 'error' event, sets a server-side
    statement_timeout next to the client query_timeout, and closes idle
    clients.
    Calendar days still taken from the Node clock now come from
    CURRENT_DATE: the net-worth report's month list, the demo data dates
    (local-midnight Dates through toISOString() landed a day early) and the
    CSV export filename. shiftMonth moves to the shared calendar helpers and
    replaces the reports-only nextMonthKey.
  • Take a rule's currency from its account in the form by @John710
    The form defaulted the currency to the base currency and let the user pick
    any other, but a rule is booked in its account's currency and the API now
    rejects a mismatch. The currency is shown read-only and follows the selected
    account; opening an older rule whose currency differed relabels it with the
    account currency it was always booked in.
  • Close sharing, undo, currency and feed-limit holes by @John710
    • DELETE /recurring/:id removed entity_shares for the id before checking
      ownership and without owner_id, so any user could strip another user's
      shares. The owned rule delete now runs first, and only the owner's shares
      go with it, in one transaction.
    • Undoing a payment read the transaction outside the transaction and deleted
      it without checking the row count, so two concurrent undos reverted the
      balance twice. DELETE ... RETURNING inside the transaction is now the check.
    • Every booking path moves the account balance by the rule amount as-is,
      while the plan report converts by the rule currency: a 10 USD rule on a RUB
      account debited 10 RUB. Create now stores the account currency and rejects
      any other (400 errors.rule_currency_mismatch); PATCH/PUT that touch the
      account or the currency are held to the same match. Existing mismatched
      rules keep booking as before and are relabelled on their next edit.
    • Shared rules exposed the owner's account name even when the account itself
      was not shared; account_name is now null unless the account is visible.
    • The calendar feed rate limit was keyed by the token, so every guessed
      token got a fresh bucket; it is now keyed by client address.
  • Honour CSV options and validate every row before writing by @John710
    Multipart is registered without attachFieldsToBody, so request.body was
    undefined and the options were never read: "Preview" ran a real import. The
    handler now iterates request.parts(), accepts the options field before or
    after the file, and only writes when the options say previewOnly: false.
    Rows are validated before the insert transaction opens, and each bad row is
    reported in stats.errors as { row, error } instead of aborting or vanishing:
    • amounts are parsed locale-aware into decimal.js (spaces/NBSP/apostrophes
      are grouping, the last '.' or ',' is the decimal point, groups of three),
      garbage is a row error;
    • dates honour dateFormat (MM/DD/YYYY was unreachable), impossible days are
      row errors, an unsupported dateFormat is a 400;
    • a currency column that differs from the account currency, a transfer row
      (no target account) and a named account the user does not have are row
      errors;
    • categories are only matched to existing ones by name and type; unknown
      names stay uncategorized (stats.uncategorized) instead of creating a global
      category every user would see;
    • balance deltas are summed in decimal.js.
      Also adds the backend error strings for the recurring currency check.
  • Clean up recurring links before migration 32's constraints by @John710
    v1.3.2 deleted rules without clearing transactions.recurring_rule_id and
    could book an occurrence twice, so upgrading a real 1.3.x database failed on
    the validated FK and on the unique (recurring_rule_id, date) index, and the
    container crash-looped (docker-entrypoint runs with set -e).
    up() now first unlinks transactions that point at deleted rules and, for
    duplicate (rule, date) pairs, keeps the lowest id linked and unlinks the rest.
    Nothing is deleted and no amount or balance changes; migration 35 recounts
    occurrences_paid afterwards. Editing this applied migration is a deliberate
    exception: databases where it already ran never re-run it, and for them the
    clean-up would match no rows anyway.
  • Book due rules by the database clock and isolate failures by @John710
    The hourly job selected rules with SQL next_date <= CURRENT_DATE (UTC in
    PostgreSQL) but booked them up to a Node-side today (Asia/Vladivostok in the
    container). When the two calendars disagreed a rule was selected, owed
    nothing, kept its cursor and was selected again forever; the running guard
    then skipped every later hourly run. One failing rule also rethrew out of the
    per-user loop and stopped the job for every user, every hour.
    The per-user loop now lives in services/recurring-run.js (bookDueRules): it
    reads today from SELECT CURRENT_DATE::text in the same transaction as the
    selection, sets aside a rule whose state did not advance, and catches,
    logs and sets aside a rule whose booking failed so the user's other rules and
    the other users still run.
  • Ignore protocol-relative redirect targets by @John710
    A ?redirect=//host value passed the leading-slash check.
  • Address audit findings in saving, undo, charts, auth and modals by @John710
    Users could create duplicate transactions by double-clicking or pressing
    Ctrl+Enter, and API validation errors were silently swallowed. Several
    other paths failed quietly or showed wrong data.
    • guard save/delete handlers with a busy flag and toast API errors in
      Transactions, Budgets, Debts and Accounts; add a global Vue errorHandler
    • undo bar follows the current route; drop undo for account delete (it only
      recreated an empty account); transaction undo keeps tags and splits and is
      replaced by a stronger confirm when attachments exist
    • escape user strings in ECharts tooltips; give sankey nodes unique ids so a
      shared income/expense category name no longer breaks the DAG
    • unsubscribe web push on logout; sync base currency in checkAuth
    • ConfirmModal: localized defaults, focus Cancel, danger on delete confirms
    • CSV export goes through api() (401 refresh, blob support) with error toast
    • ImportCsvModal: options before file, real tab delimiter, stats.errors,
      emit imported only on success, handle profile load failure
    • command palette: unique keys, open the add modal via ?new=1 after routing
    • focus trap: no scroll unlock from closed modals, stacked modals, cancel
      pending frame
    • sanitizeSvg forbids style and a; service worker caches only ok shells and
      tolerates non-JSON push payloads
    • api(): keep ?redirect= on session loss, localized messages
    • AccountDetail uses its own list, refetches on :id change, load more
    • notifications rows are buttons, store calls are caught
    • rates: show unconvertible amounts in their own currency
    • i18n: totp_required, KB, TOTP QR alt and the new strings above
  • Keep personal API tokens away from password and 2FA routes by @John710
    A leaked API token could change the password, disable 2FA or sign out
    every session, locking the owner out of their own account.
  • Close token, session, 2FA and SSRF gaps found in the audit by @John710- Repair dashboard, reports, budgets, debts, export and rate refresh by @John710- Convert USD into crypto currencies by @John710
    Rates are stored as USD->fiat and crypto->USD. A USD source has no
    USD->USD row, so convertAmountDecimal found neither the via-USD nor the
    inverse path and USD->BTC returned null while EUR->BTC worked. The USD
    leg of a USD source is now the identity.
  • Repair category binds and lock down global catalog writes by @John710
    Creating or updating a transaction with a category, splits and category
    merge all sent more bind values than placeholders, which PostgreSQL rejects
    with 08P01, so they answered 500 in production. The budget alert joined the
    categories.user_id column dropped by migration 29 and never fired.
    Categories and tags are global since migration 29, yet any user could
    rename, delete or merge them, rewriting every user's transactions, rules
    and budgets (a category delete cascades into everyone's budgets). Update,
    delete and merge are now admin-only (403 catalog_admin_only); creating
    stays open. Tag usage counts only cover the caller's visible accounts.
    Editing or deleting a transfer rewrote the paired leg and the counter
    account's balance with access checked on the source account only; write
    access to the counter account is now required (403 otherwise).
    Account deletion reversed transfers with UPDATE ... FROM over raw rows,
    which applies a single joined row per account, so only one of N transfers
    to the same counterparty was undone; the amounts are summed per account
    first. The account list returned a shared account twice when it had both
    a family and a personal grant; one grant (the most permissive) is picked,
    the same rule accountForWrite/accountForRead now follow. Visibility
    helpers ignore grants whose owner no longer owns the account.
    Balance arithmetic and the split-sum check use decimal.js instead of float
    math and a half-cent tolerance; the transaction count matches the rows the
    list pages through (split children included).
    The test stub db now fails any statement whose bind array does not match
    its highest $n placeholder, so this class of bug cannot pass tests again.
  • Harden compose secrets, image init, SSL migrations and CI tests by @John710
    Ship no usable default secrets: .env.example leaves POSTGRES_PASSWORD,
    JWT_SECRET and ENCRYPTION_KEY empty and compose requires them (${VAR:?}),
    so a copied example can no longer yield forgeable admin tokens or a
    known DB password. Forward SHOUTRRR_BLOCK_PRIVATE_TARGETS, COINGECKO_API_KEY,
    LOG_LEVEL and PUSH_SEND_TIMEOUT, which the app reads but compose dropped.
    Run the image under tini with a HEALTHCHECK start-period, drop tests and
    tooling from the build context, and switch su-exec to uid:gid so PGID is
    honoured when PUID is an existing user. Map DATABASE_SSL* to PGSSLMODE for
    node-pg-migrate, run backend/frontend vitest in the CI lint job, add
    dependabot docker-compose and minor/patch grouping, and fix README quick
    start, env tables and API list (EN and RU).
  • Keep the recurring section named Регулярные everywhere by @John710
    The nav label and the dashboard cards already said Регулярные; the page heading
    still carried the older Повторяющиеся.

CI/CD

  • Use a high-entropy JWT secret for the blackbox stack by @John710
    The startup check now rejects low-entropy secrets; the repeated
    ci-jwt-secret string would stop the CI container from booting.

Features

  • Follow up on backend audit fixes in admin, catalog, debts and import UIs by @John710
    The backend now resets 2FA for admins, makes categories and tags admin-only, locks debt booking fields once payments exist, and returns richer CSV import results. The UI had to catch up so users see the right controls and server messages instead of silent 403/409 failures.
    • admin users list shows 2FA status and a danger-confirmed Reset 2FA action
    • hide category edit/merge and tag edit/delete for non-admins
    • recurring: danger delete confirm, saving/deleting guards, error toasts
    • debts: edit form (locked amount/currency/type/account when payments exist), empty due_date no longer sent
    • api(): keep a route's own localized 429 message (rates refresh throttle) while still replacing framework rate-limit text
    • CSV import: date format select defaulting to the profile, uncategorized count, per-row error list

Performance

  • Add the indexes the report and sharing queries need by @John710
    Adds (IF NOT EXISTS) indexes no earlier migration covers:
    transactions(account_id, date DESC), transactions(transfer_to_account_id),
    exchange_rates(from_currency, to_currency, updated_at DESC),
    notifications(user_id, created_at DESC), debt_payments(debt_id),
    budgets(category_id), budget_tags(tag_id), recurring_rules(account_id),
    debts(account_id).
    exchange_rates.rate is widened from numeric(15,6) to numeric(30,18):
    six decimals flatten small crypto prices. down() narrows it back and
    drops the indexes.

Full Changelog: v1.3.2...

Don't miss a new finapp release

NewReleases is sending notifications on new releases.