Repository navigation
Releases: amosgeva/PortfolioDB
Release list
PortfolioDB 1.7.5
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.00and 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 asn/aand 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
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-estimateswithout--since
once if it was used with--sinceon 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.pyandtest_data_quality_sql.pytalk SQL to a real
database and skip when none is configured; the only CI job with a database
ran the MCPslowsuite alone, so those cases had never executed there. The
job now runs them too, withPORTFOLIODB_TESTS_REQUIRE_DB=1making a skip
for lack of a database a failure (one shareddb.connect_for_tests). The FD
store tests read committed synthetic fixtures underapp/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 addedquantity × (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
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--sinceonce 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 aTypeError.
Every account boundary now uses one sort key (unnamed first,Noneand
""kept distinct) and unnamed accounts print as(no account)instead
ofNone. 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 aqty Δ +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
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 --backfillon 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-estimatesfor that
symbol; it deletes thesource='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=1must now addPORTFOLIODB_PASSWORD
to themcpservice indocker-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. Themcpcompose service inherited the shared environment
block, and with itPORTFOLIODB_PASSWORD, because the pool read the
database address throughload_config(), which insists on that password —
so the one container built to hold only aSELECT-only role also held the
login that could write. The address now comes from a credential-free
db.load_target(), the compose file givesmcpthe address and its own
settings only, and the read-only path never looks at the write password.
The deliberatePORTFOLIODB_MCP_ALLOW_RW_FALLBACK=1opt-out still works but
you now supplyPORTFOLIODB_PASSWORDto 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 ofTrade Date,Purchase PriceandQuantityfilled 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-errorcounts 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
Nonewith 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, soNoneand 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-estimatesdeletes the symbol'ssource='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_positionswith anas_ofbefore 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.00on 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 acorporate_actionsrow see
no change.
PortfolioDB 1.7.1
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
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 SELLand 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 asoversells. get_data_qualityreports 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
innerHTMLunescaped; they are built with the DOMOptionconstructor
now. The logo cache turned any symbol into<symbol>.pngunder
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 andadd-lotaccept 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-slimtag moved underneath it. Nowapp/constraints.txtholds
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 freezediffers 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 runspip-auditagainst 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 whenPORTFOLIODB_MCP_RO_USERwas unset, protected
only bydefault_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 themcpservice 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 ranmake 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 aREAD WRITEtransaction. -
The advisor's base URL is read from
LLM_BASE_URLin.envonly; 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 pointopenaiat a server they controlled, trigger a
brief, and receiveOPENAI_API_KEYas 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 — anLLM_BASE_URLthat points the provider
elsewhere sends the genericLLM_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
.envasLLM_BASE_URL=…and restart. The old row is ignored, logged once,
and deleted the next time Settings is saved. -
The MCP server's unauthenticated
/healthznow answers onlyok,db
andlast_snapshot_age_s. It used to return the fullget_health
payload: the last collector run'serrortext 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 theget_healthtool. 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.mdnow actually do that. Both examples were plain lists,
and Compose merges an override'sportsinto 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 inherited0.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 anddocker-compose.override.yml.examplenow use
ports: !overrideandports: !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 configthat no0.0.0.0entry 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..dockerignoreexcluded the root.envand nothing else,
while the Dockerfile copies the wholeapp/tree — so an operator who kept
the host-Python sidecarapp/.env(database password, MCP token, vendor API
key) and randocker compose buildgot an image with the file in it. Git's
ignore rules never applied to a Docker build context. Every.envsidecar
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 publishedghcr.ioimage 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_statsfor 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 newbasisfield (twr_growth_curve; the review
snapshot carries it asrisk.drawdown_basis), and itspeak/troughare
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 adegradedlist 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 withfloat(), which acceptsNaN
andInfinity. NaN is neither< 0nor== 0, so it passed the
importer's checks, and PostgreSQL's numeric NaN sorts above every finite
value, so it passedCHECK (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: exactDecimal, finite,
within the twelve integer digitsNUMERIC(20,8)holds, sign per column.
ACurrent Pricethat is not finite imports as "no price". Upgrading:
runmake schema(ordocker compose run --rm dashboard python app/apply_schema.py). Migration `003_finite_numeric...
PortfolioDB 1.6.0
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_accountinget_portfolio_review_snapshot. The
per-account breakdown behindsummary.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.buildaccepted atodayargument 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 omittodaykeep 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
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.ymlstarts only the
postgresservice, the Python suites run on the host runner, andmake test
runs against the pulled image becausedocker-compose.ymldeclares
image:and notbuild:. 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 buildfailed 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-sliminstead of
python:3.13-slim. Verified in the built image: both suites pass, every
pin imports, supercronic is present with a valid crontab, andzoneinfo
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-slimand
python:3.14-slimcarried the identicalutil-linux 2.41.5-0+deb13u1, and
apt-cache policyinside the image reported that same version as the newest
available, fromtrixie-security. There was no patched util-linux to move
to, so no Debian-based tag clearedSNYK-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
forapt-getin three places anduseraddin 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
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,backupandrestore. 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 singledocker composecommand 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 withoutmake, and CI now fails if a
Makefile target exists that appears in neither this page norpdb.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
curlis an alias forInvoke-WebRequest, which rejects those flags),
notepad, andicacls .env /inheritance:rin place ofchmod 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.ps1reports "running scripts is disabled
on this system" until you allow it.
- Also the first-run wall nobody warns you about: Windows refuses to run any
-
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./databind-mount override at a/mnt/cpath.
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 > fileto Windows fails twice: there is no hostgzip, 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 -trejects the
file, and otherwise you find out on the day you restore.pdb.ps1 backupcompresses inside the container and moves the finished
file out withdocker compose cp, so no binary data crosses the host shell.
docs/commands.mddocuments that form for anyone doing it by hand, and CI
asserts the round trip withgzip -trather 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 restorekeeps 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
anddocs/exposure.mdnow say wheremakedoes not apply and what to run
instead.docs/operations.mdgains 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.gzAn untested backup is a hypothesis, and this is the release that says which
hypothesis was wrong.
PortfolioDB 1.3.0
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.serveris 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--hostto uvicorn itself rather than going through the runner,
so containerised deployments are unaffected either way.
- If you rely on reaching the MCP server from another machine, set
Security
python-dotenvfloor raised to 1.2.2 (CVE-2026-28684, arbitrary file
overwrite via symlink following). The old>=1.0range 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 tourlopen, which would otherwise read a local file and report
on it as though it were the live site. .gitignorenow covers every.envsidecar, not just the known suffixes.
Copying your.envbefore editing it —cp .env .env.bak— is the obvious
precaution, and under the old exact-match rules that copy was untracked but
not ignored, onegit add -Aaway from committing your credentials.
.env.templatestays visible.
Fixed
- Nine
try/except/passblocks replaced withcontextlib.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_HOSTand passes--hostto uvicorn itself, so it never uses
the changed default. - Running
python -m app.mcp.serverdirectly — set
PORTFOLIODB_MCP_HOST=0.0.0.0in 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.