Skip to content

Releases: amosgeva/PortfolioDB

PortfolioDB 1.7.5

Choose a tag to compare

@amosgeva amosgeva released this 10 Sep 11:05
f4dd7d9

The fifth pass over the audit closes its last open finding: the weekly
report's contribution for a position sold during the week, which had no
closing quote to be valued against and so printed as zero. The figure now
comes from the value-and-flow identity directly, so a closed position needs no
quote and a held one without its quote says so instead of reading as zero.
No schema change and no migration.

Upgrading

  • Nothing to do. Weekly contributor lines change only for symbols sold during
    the week with no closing quote, and for held symbols missing a quote (now
    n/a); totals are unchanged.

Fixed

  • A position sold during the week keeps its weekly contribution when it
    has no closing quote.
    The collector stops quoting a symbol once it is
    sold, so a position closed during the week normally has no end-of-week
    quote; the contribution arithmetic skipped every trade without one, so a
    full sale printed $0.00 and a buy-and-sell round trip vanished from the
    contributor list while the week's total was right. The contribution is now
    computed from the identity directly — end value − start value + proceeds −
    outlays — where an endpoint with no shares is worth zero and needs no
    quote. A held position that lacks its quote is printed as n/a and named
    under the missing-price warning, never as $0.00. The price-move and
    trade-P&L breakdown is still shown when both quotes exist
    (1.7.4 re-audit, N05 remaining branch).

PortfolioDB 1.7.4

Choose a tag to compare

@amosgeva amosgeva released this 10 Sep 09:12
bc5df60

The fourth pass over the audit: the two findings the re-audit of 1.7.3 added.
The weekly report's contributor arithmetic credited a sale's move twice and
counted a baseline-day trade as new, which round 3's flat-price fixtures could
not see; and 35 SQL integration tests had never run in CI because the only job
with a database ran the slow suite alone. The weekly report also stops taking
futures-only market-overview snapshots as its week boundaries. No schema
change and no migration.

Upgrading

  • Nothing to do. If you run the weekly report yourself, expect the
    contributor amounts to change where the week held sales or trades dated on
    the start snapshot's day; the week's totals were already right.
  • The 1.7.3 Upgrading step (rerun --replace-estimates without --since
    once if it was used with --since on 1.7.2) still applies if not done.

Changed

  • The 35 SQL integration tests now run in CI. test_dedupe_guards.py,
    test_fd_store.py and test_data_quality_sql.py talk SQL to a real
    database and skip when none is configured; the only CI job with a database
    ran the MCP slow suite alone, so those cases had never executed there. The
    job now runs them too, with PORTFOLIODB_TESTS_REQUIRE_DB=1 making a skip
    for lack of a database a failure (one shared db.connect_for_tests). The FD
    store tests read committed synthetic fixtures under app/tests/fixtures/fd/
    instead of the gitignored enrichment cache, so a clean clone exercises every
    round trip (1.7.3 re-audit, N06). The four unused test imports pyflakes kept
    listing are gone.

Fixed

  • Weekly contributor P&L no longer double-counts sales or baseline-day
    trades.
    The contribution gave every opening share its full start-to-end
    move and then added quantity × (sale − start) for the shares sold during
    the week, so a sale earned the same move twice: selling 5 of 10 at $110
    with quotes $100 → $120 printed $250 for a $150 gain, and a full
    liquidation or a round trip were off by the same amount. A trade dated on
    the start snapshot's day was also counted as both part of the opening
    position and a new trade. The sale term now compares against the closing
    quote, contributions count only trades dated after the baseline day (the
    trades listing still shows the calendar week and says so), and with cash
    mirroring the trades the symbol contributions sum to the change in
    securities plus cash. The report's week boundaries are now taken from
    snapshots that quote a held symbol, so a futures-only market-overview run
    can no longer become the start or end of the week and value every holding
    as missing (1.7.3 re-audit, N05; boundary defect found in round 3).

PortfolioDB 1.7.3

Choose a tag to compare

@amosgeva amosgeva released this 10 Sep 08:28
bae6289

The third pass over the audit: the three findings the re-audit of 1.7.2 left
open. Two are the remaining branches of cases 1.7.2 fixed at the reported
sites only — the weekly and executive reports still mixed split units in
other calculations, and the weekly report still crashed on an unnamed account
one section after the fixed helper — and one is a scope bug in the
--replace-estimates flag 1.7.2 added. The tests for the reports now run the
whole command rather than its helpers. No schema change and no migration.

Upgrading

  • If you ran add_income.py --backfill --replace-estimates --since … on
    1.7.2, the estimates dated before the cutoff were deleted and not rebuilt.
    Rerun the same command without --since once on 1.7.3 to restore them;
    manual rows are never touched.
  • Nothing else. The 1.7.2 Upgrading steps still apply if you have not
    done them.

Fixed

  • add_income.py --backfill --replace-estimates --since … no longer
    deletes estimates before the cutoff.
    The deletion covered every
    backfilled row for the symbol while the rebuild honoured --since, so a
    bounded correction removed the older estimates and did not put them back.
    The two halves now share one scope: with --since, only estimates dated on
    or after it are deleted; nothing is deleted when the vendor series has
    nothing to rebuild in that range; and the command was already one
    transaction, so a failed insert restores the deleted rows. Manual rows are
    never touched (1.7.2 re-audit, N04).

  • The weekly report completes with unnamed accounts present. 1.7.2 fixed
    the account sort inside the positions helper, but the account-totals loop
    in the report itself sorted the same union of accounts (plus the cash
    accounts) with the default ordering, so an unnamed lot beside a named
    account still stopped the report two sections later with a TypeError.
    Every account boundary now uses one sort key (unnamed first, None and
    "" kept distinct) and unnamed accounts print as (no account) instead
    of None. The test runs the whole command, not the helper
    (1.7.2 re-audit, N01 remaining case).

  • The weekly and executive reports state splits in one unit basis. The
    weekly report valued its start and end positions in each date's units but
    multiplied them by raw snapshot quotes, and fed raw trade rows into its
    contribution arithmetic, so a pure 2:1 split inside the week printed the
    holding as a 50% loser with a qty Δ +10. The executive report restated
    its lots and its EOD series but joined the raw latest quotes, so a symbol
    whose newest snapshot predated a recorded split was worth twice its value
    with the whole excess as unrealized profit. Both reports now prepare one
    ledger and pass every quote and every week trade through it; the weekly
    report lists the week's corporate actions in their own block, and the
    trades block still shows what was entered. A pure split contributes $0
    and changes neither value nor P&L (1.7.2 re-audit, F03 remaining cases).

PortfolioDB 1.7.2

Choose a tag to compare

@amosgeva amosgeva released this 10 Sep 07:24
5709b27

The second pass over the 1.7.x audit: six findings the re-audit of 1.7.1
turned up, each a case the first pass had reasoned about and got wrong at
one edge — a split dated after the observation, a dividend paid before a
later split, a liquidated stretch on the chart, an unnamed account beside a
named one, a half-filled CSV trade row, and the write password sitting in the
one container built not to have it. No schema change and no migration.

Upgrading

  • If you ran add_income.py --backfill on 1.7.0 or 1.7.1 for a symbol that
    split after one of its dividends, the estimates for the earlier dividends
    are undercounted. Rerun with --backfill --replace-estimates for that
    symbol; it deletes the source='yfinance' rows first and says how many it
    replaced. Manual income rows are never touched.
  • Compose deployments that run the MCP server on
    PORTFOLIODB_MCP_ALLOW_RW_FALLBACK=1 must now add PORTFOLIODB_PASSWORD
    to the mcp service in docker-compose.override.yml
    (exposure shows the block). Deployments
    on the read-only role, which is the default, need nothing.
  • A CSV export with half-filled trade rows that imported clean before will
    now be rejected with the line numbers; fix the rows or blank all three
    trade fields to make them quote-only.

Security

  • The MCP container no longer receives the application's read-write
    password.
    The mcp compose service inherited the shared environment
    block, and with it PORTFOLIODB_PASSWORD, because the pool read the
    database address through load_config(), which insists on that password —
    so the one container built to hold only a SELECT-only role also held the
    login that could write. The address now comes from a credential-free
    db.load_target(), the compose file gives mcp the address and its own
    settings only, and the read-only path never looks at the write password.
    The deliberate PORTFOLIODB_MCP_ALLOW_RW_FALLBACK=1 opt-out still works but
    you now supply PORTFOLIODB_PASSWORD to the service yourself in
    docker-compose.override.yml (exposure
    shows the block); without it the server refuses to start and says so.

Fixed

  • CSV import rejects incomplete trade rows instead of skipping them. A
    row with some of Trade Date, Purchase Price and Quantity filled in
    was treated as "not a lot": nothing was imported for the trade, its price
    snapshot still went in, and the run reported zero rejections — a trade
    missing from the ledger behind a clean import. Such a row is now rejected
    with its line number and the missing field, and so is a trade row with no
    symbol; a row with all three trade fields blank is still a quote-only row,
    and a wholly blank line is still skipped. The transaction policy applies:
    atomic mode writes nothing from that file, --continue-on-error counts the
    row as rejected and writes the rest.

  • The weekly report no longer crashes when an unnamed account meets a named
    one.
    1.7.0 moved its position aggregation into Python and sorted
    (account, symbol) keys with the default ordering, which refuses to compare
    None with a string; any ledger with both an account-less lot and a named
    account stopped the report before it printed. Unnamed accounts now sort
    first, and the stored value is kept as is, so None and an empty-string
    account stay distinct.

  • The portfolio-value chart keeps the days on which everything was sold.
    The history dropped every zero-valued point, so a liquidated stretch
    vanished from the chart and the line bridged from the last funded day to
    the re-entry. Only the points before anything was ever held are dropped
    now; a zero after that is drawn as a flat line at zero, and a range that
    begins on such a day shows no percentage change rather than a division by
    zero.

  • Dividend backfill: a dividend paid before a later split is no longer
    undercounted.
    yfinance states every historical per-share dividend in
    today's split-adjusted units (Apple's $0.82 of August 2020 comes back as
    $0.205 after the 4:1), so the shares it is multiplied by must be in today's
    units too. 1.7.0 read the ledger as of the ex-date, which left a later split
    out and recorded half (or a quarter) of the cash for every dividend paid
    before one. The backfill now selects lots by ex-date and counts them in
    current units (ledger_inputs.load(units="current")). Existing
    estimates:
    rows written by earlier backfills carry the old amounts, and
    because the dedupe key includes the amount a rerun would insert the
    corrected row beside the old one. add_income.py --backfill --replace-estimates deletes the symbol's source='yfinance' rows first
    and reports how many it replaced; manual rows are never touched.

  • Historical MCP positions and the daily/EOD report state splits in the
    right units.
    get_positions with an as_of before a recorded split
    applied the split anyway, so "the day before a 2:1" reported twenty shares
    against the pre-split quote — twice the value that existed; a stale quote
    observed before an ex-date the cutoff was past was joined to restated
    shares the same way. The daily/EOD report restated its lots but compared
    raw previous, day-start and current quotes, so a split between two quotes
    printed a loss of the whole ratio (Delta: $-1,000.00 on a pure 2:1 with
    nothing else moving). Actions now apply only when dated on or before the
    observation date (ledger_inputs.prepare(as_of=…), shared by every
    date-filtered reader), stale quotes are restated into the cutoff's units,
    and the report's three comparison quotes go through the prepared ledger
    with their own timestamps. Installs without a corporate_actions row see
    no change.

PortfolioDB 1.7.1

Choose a tag to compare

@amosgeva amosgeva released this 10 Sep 05:29
90f1fba

A one-line fix for a 1.7.0 regression that broke every dashboard page. No
schema change and no migration; the 1.7.0 Upgrading steps still apply if
you have not done them.

Fixed

  • 1.7.0's dashboard failed to load with "too many values to unpack
    (expected 2)".
    The news feed's normal path still returned a bare list
    after its contract changed to (rows, problem) in 1.7.0; the payload test
    had stubbed the whole function and so never ran that line. The test now
    stubs only the news store, so the section's real code runs. Anyone on 1.7.0
    sees the error banner on every page; 1.7.1 is the fix.

PortfolioDB 1.7.0

Choose a tag to compare

@amosgeva amosgeva released this 09 Sep 19:33
4f30cfb

The release that acts on the 2026-09-09 codebase audit: every High and Medium
finding, and the routine items behind them. Read Upgrading before pulling —
this one has a migration, a new requirement for the MCP server, and several
figures that change on purpose.

Three things move numbers. Recorded splits now reach the dashboard, the CLI and
the reports, not only the MCP tools. Time-weighted return values a sale at its
own price, so closing a position no longer reads as 0% or −100%. Portfolio
drawdown is measured on the flow-adjusted growth curve, so a withdrawal is no
longer a drawdown. Each has its own entry below with the before and after.

Minor rather than major because every upgrade step has a documented path the
operator can take (make schema; make ro-role or the explicit fallback
flag; two lines moved to .env), and nothing about the ledger's data has to be
rewritten. It is the largest minor this project has shipped, and the entries
are long because you are entitled to know what changes before you run it
against your own records.

Added

  • A sale bigger than the position is warned about when it is entered.
    sell-lot, add-lot --side SELL and the CSV importer check the prepared
    ledger for the account as of the trade date and print what is held versus
    what is being sold, then still record the row: the usual cause is the wrong
    account, a mistyped date or a missing BUY, and the row is the evidence. The
    FIFO engine's read-time truncation warning stays; this is the same fact,
    said to the person typing. The importer counts these as oversells.
  • get_data_quality reports an instrument whose registered currency is
    not the reporting currency
    (foreign_currency, a correctness issue at
    any position size): the ledger sums face values and nothing converts, so a
    EUR position in a USD ledger makes every total wrong. Previously nothing
    checked.

Security

  • Symbols reach the page as text and the disk only when they look like
    symbols.
    The dashboard's two symbol <select>s interpolated symbols into
    innerHTML unescaped; they are built with the DOM Option constructor
    now. The logo cache turned any symbol into <symbol>.png under
    app/dashboard/static/logos/, writing (the fetcher) and reading back into
    the page as a data URI (the dashboard); both go through one allowlist that
    admits what real symbols look like (BRK.B, ^GSPC, ES=F, 0700.HK) and
    nothing with a path separator. A symbol is operator-entered text — the
    importer and add-lot accept any string — so it is treated as such.

  • The published image is reproducible and scanned. app/requirements.txt
    is ranges; the image resolved them afresh on every build, so a passing test
    run said nothing about the image a user pulled a week later, and the base
    python:3.14-slim tag moved underneath it. Now app/constraints.txt holds
    the exact set the image was tested with (generated inside the image with
    make lock), the Dockerfile installs with it, CI fails if the image's
    pip freeze differs from the file, the base image is pinned by digest with
    Dependabot proposing bumps for it and for the pinned actions, and a weekly
    workflow runs pip-audit against the lock and Trivy against the built
    image (fixable HIGH/CRITICAL findings fail it). Provenance attestations stay
    off, with the reason recorded next to the setting. No runtime change: the
    pins are what the last green build already contained.

  • The MCP server requires its read-only database role and receives only the
    environment it needs.
    The pool fell back to the application's read-write
    credentials silently when PORTFOLIODB_MCP_RO_USER was unset, protected
    only by default_transaction_read_only=on — a session setting any
    statement can switch off. It now refuses to connect without the role and
    says how to create it; the fallback survives as an explicit, logged opt-out
    (PORTFOLIODB_MCP_ALLOW_RW_FALLBACK=1). In compose the mcp service no
    longer inherits the whole .env: it gets the database settings and its own
    keys, not the LLM keys or the vendor API key. Upgrading: if you run the
    MCP server and never ran make ro-role, run it now and add the two lines
    it prints to .env, or set the fallback flag.
    Installs that already set
    the role see no change. A live test now proves the role refuses a write on
    privilege even inside a READ WRITE transaction.

  • The advisor's base URL is read from LLM_BASE_URL in .env only; the
    Settings-page field is gone.
    The page has no login, and the base URL is
    where the OpenAI-compatible client sends the API key: anyone who could open
    the dashboard could point openai at a server they controlled, trigger a
    brief, and receive OPENAI_API_KEY as a Bearer header. A URL from an
    unauthenticated form must never decide where a secret goes. In addition a
    vendor's named key (OPENAI_API_KEY, OPENROUTER_API_KEY) now only travels
    to that vendor's own origin — an LLM_BASE_URL that points the provider
    elsewhere sends the generic LLM_API_KEY, or fails with a message naming
    the two variables. Upgrading: if you had set a base URL on the Settings
    page (Ollama on the Docker host is the common case), put the same value in
    .env as LLM_BASE_URL=… and restart. The old row is ignored, logged once,
    and deleted the next time Settings is saved.

  • The MCP server's unauthenticated /healthz now answers only ok, db
    and last_snapshot_age_s.
    It used to return the full get_health
    payload: the last collector run's error text verbatim — which names the
    symbols that failed and carries a traceback when yfinance raised — plus
    per-table enrichment counts and the database's own failure reason (role
    and container IP). Anyone who could reach the port learned part of the
    portfolio universe without a token. The full diagnostic is unchanged behind
    the bearer token as the get_health tool. Monitors that parsed the old
    body need the new three keys; the status code contract (200 up, 503 down)
    is the same, so the compose healthcheck is unaffected.

  • The "localhost only" and "stop publishing Postgres" overrides in
    docs/exposure.md now actually do that.
    Both examples were plain lists,
    and Compose merges an override's ports into the base file's list rather
    than replacing it, keying entries on host IP as well as port — so the
    loopback mapping landed beside the inherited 0.0.0.0:8501, and
    ports: [] removed nothing. An operator who followed the guide had a
    dashboard that was still open to the network while their override said
    otherwise. The examples and docker-compose.override.yml.example now use
    ports: !override and ports: !reset [] (Compose 2.24.4+), and a test
    renders every documented override and fails if a wildcard mapping survives.
    If you copied the old example, re-copy it and check with
    docker compose config that no 0.0.0.0 entry remains. Nothing about the
    base file's defaults changed.

  • A source build from a populated working tree no longer bakes app/.env
    into the image.
    .dockerignore excluded the root .env and nothing else,
    while the Dockerfile copies the whole app/ tree — so an operator who kept
    the host-Python sidecar app/.env (database password, MCP token, vendor API
    key) and ran docker compose build got an image with the file in it. Git's
    ignore rules never applied to a Docker build context. Every .env sidecar
    is now excluded recursively, along with the cached ticker logos, backups and
    docs/internal/; CI plants fake secrets in those places and fails if any
    reaches a layer. The published ghcr.io image was never affected: it is
    built from a clean checkout. Only operators who built and shared a local
    image need to consider that image compromised.

Changed

  • Portfolio drawdown is measured on the time-weighted growth curve, not on
    market value.
    get_drawdown_stats for the whole portfolio ran on the
    market-value series, which falls when securities are sold and rises when
    money is added — a withdrawal read as a drawdown and a deposit could hide
    one. It now runs on the same daily flow-adjusted curve as the period
    returns, says so in a new basis field (twr_growth_curve; the review
    snapshot carries it as risk.drawdown_basis), and its peak/trough are
    index levels with 1.0 at the first observation rather than dollars. A single
    symbol's drawdown is unchanged (basis: "price"), and
    holdings_basis="current_constant" keeps the old market-value definition
    for comparison. The portfolio's max and current drawdown figures change
    for any ledger with sales or purchases in its history.

  • The dashboard names a section it could not load instead of rendering it
    empty.
    The market strip and the news feed swallowed a failed query into an
    empty list; the payload now carries a degraded list and the page shows a
    banner naming the section and the error type.

  • Every number written to the ledger must be finite, in range, and the
    right sign — and the database now refuses NaN too.
    The CSV importer and
    the write CLIs (add-lot, sell-lot, set-cash, add_income) parsed
    quantities, prices, fees and amounts with float(), which accepts NaN
    and Infinity. NaN is neither < 0 nor == 0, so it passed the
    importer's checks, and PostgreSQL's numeric NaN sorts above every finite
    value, so it passed CHECK (quantity > 0) and was stored. Infinity failed
    later at the column type, aborting the importer's transaction. One parser
    (app/ledger_numbers.py) now handles all of them: exact Decimal, finite,
    within the twelve integer digits NUMERIC(20,8) holds, sign per column.
    A Current Price that is not finite imports as "no price". Upgrading:
    run make schema (or docker compose run --rm dashboard python app/apply_schema.py). Migration `003_finite_numeric...

Read more

PortfolioDB 1.6.0

Choose a tag to compare

@amosgeva amosgeva released this 09 Sep 15:07
1979982

The review snapshot gains a per-account cash breakdown, and the period
statistics stop holding back a finished week or month until the next one has a
snapshot. No schema and no migration — see Upgrading.

Minor rather than patch because a field was added to an MCP tool's output:
backward compatible, but something new for a client to consume, which a patch
should not carry.

Added

  • summary.cash_by_account in get_portfolio_review_snapshot. The
    per-account breakdown behind summary.cash — the latest balance each account
    had entered at the cutoff — was computed on every call and then discarded. It
    is now returned next to the total it explains, so a reader can see which
    account holds the cash. Additive: no existing field moved or changed meaning.

Changed

  • Period statistics release a finished week or month as soon as the date has
    moved past it.
    period_stats.build accepted a today argument and never
    read it, so the last group in the curve was always treated as still running
    and kept out of best/worst. A complete August therefore stayed excluded until
    September's first snapshot landed — every 1st of the month before the
    collector ran, and every Monday morning for the week. The dashboard passes
    today; with it, a period whose calendar end is behind today counts as
    complete. Callers that omit today keep the old behaviour. This can change
    the "best month" / "worst week" records shown during that window, and only
    then.

Upgrading

No migration. docker compose pull && docker compose up -d.

The rest is the first round of fixes from the Sonar way quality profile:
build_payload_data, the prompt registry and the CSV importer's main are
split into smaller functions, and the tests that wrapped several calls in one
pytest.raises block now isolate the call under test. No figure changes from
any of that
: the dashboard payload built by the old and new payload.py
against the same live database was diffed field by field and is identical.

PortfolioDB 1.5.0

Choose a tag to compare

@amosgeva amosgeva released this 07 Sep 08:45
8e3e122

The application image moves to Python 3.14, and CI now builds that image before
a change to it can be merged. No figure changes, no schema and no migration —
see Upgrading.

Minor rather than patch because the interpreter the image ships is part of what
it delivers, not an implementation detail: anything built FROM this image that
installs a wheel with no 3.14 build breaks across this boundary, and a patch
should not be able to do that. Nothing inside the application changed.

Added

  • CI builds app/Dockerfile, then runs the suites inside the result. Until
    this release nothing in CI ever built the image. ci.yml starts only the
    postgres service, the Python suites run on the host runner, and make test
    runs against the pulled image because docker-compose.yml declares
    image: and not build:. The publish workflow does build it — after merge.

    So a change to the Dockerfile could pass every check without anyone learning
    whether the image still built, and one did: a scanner's base-image PR
    swapping Debian for Alpine went green on all seven checks while
    docker build failed at the second of thirteen steps with
    apt-get: not found. The new job fails on that Dockerfile and passes on this
    one, which is the only evidence worth having that the gap is closed.

    Building alone is necessary and not sufficient, so the job also imports every
    pin and validates the crontab — a base image can build cleanly and still ship
    an interpreter your pins have no wheels for, which was true of that same PR.
    It builds amd64 only; the publish workflow covers arm64.

Changed

  • The application image is built on python:3.14-slim instead of
    python:3.13-slim.
    Verified in the built image: both suites pass, every
    pin imports, supercronic is present with a valid crontab, and zoneinfo
    still resolves. pandas 3.0.5 and streamlit 1.63.0 are the versions 1.4.0
    shipped; the only other difference is numpy 2.5.2 to 2.5.3, a patch-level
    float any rebuild would have picked up.

    This is a currency bump and not the security fix a scanner will keep
    proposing it as. Verified on 2026-09-07: python:3.13-slim and
    python:3.14-slim carried the identical util-linux 2.41.5-0+deb13u1, and
    apt-cache policy inside the image reported that same version as the newest
    available, from trixie-security. There was no patched util-linux to move
    to, so no Debian-based tag cleared SNYK-DEBIAN13-UTILLINUX-17690419 — a
    medium-severity use-after-free on code paths this container never calls.
    Leaving Debian would clear it, at the cost of a port: this Dockerfile reaches
    for apt-get in three places and useradd in a fourth.

Upgrading

No migration. docker compose pull && docker compose up -d.

No figure changes. No schema, no stored value and no computation was
touched. The engines, the dashboard and the MCP server are the same code as
1.4.0 — the only file under app/ that differs is the Dockerfile, and the only
other change is the version number in server.json that the footer reports.
What moved is the interpreter underneath them.

If you build your own image FROM this one, check it still builds before you
roll it out.
A wheel pinned to cp313, or a pip install of anything with no
3.14 build, will fail where it previously worked. This is the one case in this
release that can break something.

Host installs (not compose) are unaffected. They run whatever Python you
installed, and this release does not touch app/requirements.txt.

PortfolioDB 1.4.0

Choose a tag to compare

@amosgeva amosgeva released this 03 Sep 11:49
0af87a2

A release about installing this on Windows. No figure changes, no schema and no
migration — see Upgrading — and nothing here alters an existing install on
macOS or Linux. What changes is that the documented path now works on all three
platforms, and one of the ways it previously did not was capable of losing a
backup.

The project told everyone to run make, which is a POSIX shell script runner
wearing a build tool's name. Its recipes reach for sed, base64, gzip and
/dev/urandom, so on Windows even a current make.exe cannot run six of the
thirty-one targets — including the bare make that lists them. The README's
make-free path was no better: it opened with curl -fsSLO, $EDITOR and
chmod 600, none of which do what they say on a stock Windows box.

Added

  • pdb.ps1, a Windows runner for the three targets that carry real logic:
    init, backup and restore. It runs on Windows PowerShell 5.1 — what a
    fresh Windows box actually has — as well as PowerShell 7. Everything else the
    Makefile does is a single docker compose command that is identical on every
    platform, so wrapping those would add a second thing to keep in step and buy
    nothing.

  • docs/commands.md, every target beside the command it
    runs.
    This is the reference for anyone without make, and CI now fails if a
    Makefile target exists that appears in neither this page nor pdb.ps1 — the
    drift is otherwise silent and invisible to a Linux-only build.

  • A per-platform quick start, with the Windows equivalents written out
    rather than left as an exercise: curl.exe (in PowerShell 5.1 the bare word
    curl is an alias for Invoke-WebRequest, which rejects those flags),
    notepad, and icacls .env /inheritance:r in place of chmod 600 — on NTFS
    this is an ACL, and without dropping inherited entries the grant is merely
    additive and the file stays readable.

    • Also the first-run wall nobody warns you about: Windows refuses to run any
      local script by default, so .\pdb.ps1 reports "running scripts is disabled
      on this system" until you allow it.
  • WSL2 named as the way to get the full Makefile on Windows, since Docker
    Desktop's own backend is WSL2 and has almost certainly installed it already.
    With the warning that goes with it: keep the project in the Linux filesystem,
    and do not point the ./data bind-mount override at a /mnt/c path.

Fixed

  • A Windows backup could produce a corrupt archive and tell you it had
    worked.
    This is the one item here that could have cost data. Translating
    pg_dump | gzip > file to Windows fails twice: there is no host gzip, and
    Windows PowerShell 5.1 decodes a native command's output as text before
    redirecting it, so the compressed stream is re-encoded on its way to disk.
    Measured on a real ledger, the same dump came out 5.6 MB and unreadable that
    way against 2.9 MB and valid. Nothing reports an error; gzip -t rejects the
    file, and otherwise you find out on the day you restore.
    • pdb.ps1 backup compresses inside the container and moves the finished
      file out with docker compose cp, so no binary data crosses the host shell.
      docs/commands.md documents that form for anyone doing it by hand, and CI
      asserts the round trip with gzip -t rather than checking that a file
      appeared. PowerShell 7 does not corrupt the redirect; the documented form
      works on both, so there is nothing to remember.
    • pdb.ps1 restore keeps the Makefile's guard and refuses to load a dump
      into a database that already has tables.

Changed

  • CONTRIBUTING.md, CLAUDE.md, docs/operations.md, docs/csv-import.md
    and docs/exposure.md now say where make does not apply and what to run
    instead. docs/operations.md gains a Windows Scheduled Task recipe beside the
    weekly cron entry.

Upgrading

No migration. docker compose pull && docker compose up -d.

No figure changes. No schema, no stored value and no computation was
touched. Not one file under app/ changed — the engines, the dashboard and the
MCP server are the same code as 1.3.0, and the only reason the image is rebuilt
at all is the version number in server.json that the footer reports. This
release is documentation, one new script, and tests.

Nothing to do on macOS or Linux. The Makefile is unchanged — not one recipe
was edited — so every make command keeps working exactly as before. pdb.ps1
is additive and irrelevant to those platforms.

If you already installed on Windows, the thing worth acting on is the backup
note above. Test any Windows-made dump you are relying on:

docker compose cp .\backups\your-dump.sql.gz postgres:/tmp/t.gz
docker compose exec -T postgres sh -c 'gunzip -t /tmp/t.gz && echo VALID || echo CORRUPT'
docker compose exec -T postgres rm /tmp/t.gz

An untested backup is a hypothesis, and this is the release that says which
hypothesis was wrong.

PortfolioDB 1.3.0

Choose a tag to compare

@amosgeva amosgeva released this 02 Sep 19:35
0aaa3e9

A security and hygiene release. No figure changes and no migration — see
Upgrading — but one shipped default is different, and if you reach the MCP
server from another machine you need to know which.

Changed

  • The MCP server's ad-hoc runner now binds localhost by default instead of
    all interfaces. python -m app.mcp.server is what someone types to try the
    server out, and a default that reaches the LAN is the wrong one for a process
    that answers questions about your ledger. Bearer auth sits in front either
    way — this narrows the blast radius of a misconfigured token, it does not
    close a hole.
    • If you rely on reaching the MCP server from another machine, set
      PORTFOLIODB_MCP_HOST=0.0.0.0.
      The shipped compose file already does,
      and passes --host to uvicorn itself rather than going through the runner,
      so containerised deployments are unaffected either way.

Security

  • python-dotenv floor raised to 1.2.2 (CVE-2026-28684, arbitrary file
    overwrite via symlink following). The old >=1.0 range permitted affected
    versions; a fresh install resolves to the newest match, so this closes the
    case where a resolver, a stale mirror, or an old lockfile lands on one that
    is vulnerable.
  • The drift-checking tool refuses non-HTTP(S) URLs rather than handing whatever
    it is given to urlopen, which would otherwise read a local file and report
    on it as though it were the live site.
  • .gitignore now covers every .env sidecar, not just the known suffixes.
    Copying your .env before editing it — cp .env .env.bak — is the obvious
    precaution, and under the old exact-match rules that copy was untracked but
    not ignored
    , one git add -A away from committing your credentials.
    .env.template stays visible.

Fixed

  • Nine try/except/pass blocks replaced with contextlib.suppress. All
    were best-effort cleanup — returning a connection, closing a handle, dropping
    a query parameter — and behave identically; the intent is now legible at a
    glance rather than inferred from an empty handler.

Upgrading

No migration. docker compose pull && docker compose up -d.

No figure changes. No schema, no stored value and no computation was
touched.

Read this if you reach the MCP server from another machine. Two cases:

  • Running it through the shipped compose file — nothing to do. Compose sets
    PORTFOLIODB_MCP_HOST and passes --host to uvicorn itself, so it never uses
    the changed default.
  • Running python -m app.mcp.server directly — set
    PORTFOLIODB_MCP_HOST=0.0.0.0 in your .env
    , or the server will answer
    only on localhost after this upgrade and remote clients will fail to connect.

Minor rather than patch because that default is a behaviour change someone can
be relying on, even though it is a default and overridable. The compose default
floats the major line, so a pull crosses this boundary on its own — which is
exactly why the case above is called out rather than left to be discovered.

Host installs (not compose) should re-run pip install -r app/requirements.txt
to pick up the python-dotenv floor.