feat/manager console - #2
Conversation
core depends on nothing: no I/O, no clock, no network. That is what makes results reproducible and the determinism testable. The load-bearing choices: - DKIM verification takes the DNS TXT record as an argument rather than fetching it. Selectors get retired, so a signature verifiable today is unverifiable in a year unless somebody captured the key at the time. Passing it in also keeps core pure and lets the standalone verifier run offline. - l= is a hard fail, not a warning: only a prefix of the body is signed. - Canonical serialization refuses to encode a float. Money is integer minor units; metrics compute in Decimal at a fixed scale. Float non-determinism across machines would give different roots for the same evidence. - Merkle leaves and internal nodes hash under different prefixes, and an odd node is promoted rather than duplicated — the duplicate-last construction lets two different leaf sets fold to the same root. - A tier is computed from evidence, never asserted, and a metric carries min(tier) over its inputs. min_tier([]) raises rather than inventing trust. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Storage holds only the head and a counter; every historical root lives in events, which cost a fraction of storage and are just as verifiable to anyone reading the log. seq is strictly monotonic per record, and that is the whole anti-backfill mechanism — a manager who drops a bad month leaves a gap anyone can read. It only means something if the sequence cannot be tampered with, so the first anchor claims the record and only that address can advance it. Without that, a griefer could manufacture the very gaps that are supposed to be evidence. logAccess is deliberately open to write: an access log only the platform can append to is an access log the platform can decline to append to. 9 tests, including a fuzz case over the sequence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Everything impure lives behind this boundary so core stays testable. - document and access_event are append-only by database trigger, not just by convention. An audit trail you can quietly edit is not an audit trail. - dkim_verdict.dns_txt_record stores the key as captured. That column is the reason anchoring earns its place. - No float column exists in the schema. Quantity and price are NUMERIC(38,12), matching the canonical serializer's scale so a round-trip through Postgres cannot change a hash. - Blobs are content-addressed and write-once; a put over an existing key is a no-op rather than an overwrite, because identical bytes are the same object and different bytes under the same hash is a problem to surface, not fix. - The chain client degrades to "skipped" with no key configured. A missing chain should weaken the proof, not break the app. - as_utc() re-attaches the timezone SQLite drops on read. The canonical serializer rejects naive datetimes by design; the fix belongs at the driver boundary, not in a weaker serializer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The seven steps from the plan — receive, verify, extract, normalize, reconcile, compute, commit — as one plain function. It runs inline: a message takes milliseconds to parse and Monad confirms in well under a second, so a queue would add moving parts in exchange for nothing a user would notice. Extraction runs regardless of the DKIM verdict. A tampered document still parses; it just parses as self_reported. Refusing to read it would hide the tamper rather than grade it. Rejected documents are still committed to the tree. A message we turned down is evidence of the rejection, and dropping it would let us quietly forget having seen it. The fence: - Disclosure profiles build the response additively. Filtering a full object down fails open — add a field, forget the deny-list entry, ship it to everyone. Here a new field is invisible until someone writes the line. - The NDA gate returns terms and a name. Nothing else is assembled, so there is nothing to peek at in the network tab. - Consent is written before the first byte of data is served. - Invites store only a token hash; a leaked database is not a working key. verifier/ proves a record with no access to our API: the .eml files, the captured DNS records, the Merkle proofs, and the cast call for the on-chain root. If it ever disagrees with the UI, believe the verifier. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
viem reads the anchored root through an RPC the viewer types in. A "verified" badge we computed on the server would be worth nothing, so every check in the panel runs in the tab: re-fold all leaves locally, check a server-supplied proof path against our own fold, then compare to the chain. Charts are hand-rolled inline SVG. A library that silently interpolates or drops a point would be a second, unversioned interpretation of evidence we just went to some trouble to pin down. The Merkle fold in verify.ts has to agree with core/merkle.py byte for byte — same 0x01 node prefix, same promotion of odd nodes. No webfonts: a product about working when nobody else's server answers should hold that standard for its own page. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
78 Python tests and 9 Solidity tests. The ones that carry weight: - Determinism pins a golden Merkle root as a constant in the repo, so a different machine on a different day has to agree. Building twice in one process only proves the code is not obviously random. - The DKIM corpus runs all 18 fixtures against real RSA, plus a test that fails if the verifier ever opens a socket. - TestKeyRotation is the argument for anchoring, stated as a test: an archived message fails against live DNS and verifies against the captured record. - The tamper drill flips one byte and asserts all four consequences. - Projection asserts against the serialized wire payload, not the component. Two bugs the tests found and this commit fixes: a cash flow dated after the last NAV was silently dropped from the return (now a finding), and money() in the UI hardcoded a /100 divisor that would misreport yen by 100x. The demo is staged, not typed. Three records making one argument each — and the corpus persists to disk after first generation, because regenerating keys on every boot would make each restart look like a pile of brand new evidence. Render: one web service, one Postgres, one disk. Everything wired except the two chain secrets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Product name only — no behaviour changes. The remaining occurrences live in files being rewritten anyway and land with those rewrites.
Four admin-guarded routes behind the manager's side of the product:
GET /api/records/{slug}/owner everything, no watermark
GET /api/records/{slug}/disclosure what each profile puts on the wire
GET /api/records/{slug}/preview/{prof} the exact bytes an allocator gets
GET /api/records/{slug}/invites who holds a key, and what it opens
The owner and preview views run through the same project() the allocator path
calls, at a wider profile, rather than around it. That is the point: preview and
production cannot drift, and a field invisible to every profile is invisible to
the manager too — so they find out before an allocator does.
The disclosure matrix measures rather than asserts. It runs the projection once
per profile and reports the serialized byte count and the surviving top-level
keys, so a missing section means the key is absent from the JSON, not hidden in
the UI.
Extracts _latest_snapshot/_projection_inputs from view(), which was already
assembling those five queries inline, and stamps viewer_email onto the open
payload so the page can show whose name is on the watermark.
…stances A three-minute demo should not be three minutes of typing a bearer token, so the console's login screen fills itself in. The field is None unless PODARENA_DEMO_FIXTURES is on, and a fixtures instance holds nothing but the synthetic corpus — both halves are asserted in tests/test_owner_console.py. Documents the consequence where someone deploying will actually read it: this value must never be one you would mind reading out loud while demo mode is on, and turning demo mode off is the moment to rotate it.
Rebuilds the stylesheet around named tokens (onyx, steel, umber, cream, the
dawn-arc gradient) instead of ad-hoc hex, and rewrites the components that were
reaching for colour to carry meaning.
The trust hierarchy is now encoded by weight, not hue: solid fill for signed
evidence, then outline, then quiet outline, then greyed. Same ranking on the
badges and on the chart's points. Deposits and withdrawals differ by dash
pattern. A colour-blind allocator reads the same ordering everyone else does,
and nothing on the page depends on remembering which green meant what.
Also:
- signed metrics get an explicit leading + , since there is no green to lean on
- the NAV chart uses a viewBox so it scales instead of clipping
- EvidenceTable takes `token: string | null` — the manager's preview has no
invite to download against, so the row renders and the link does not
The other half of the product. A manager signs in with the admin token, picks a
fund, and sees the thing they actually worry about: not "what do I have" but
"what did I hand out".
- DisclosurePanel draws the measured matrix — one row per payload section,
one column per profile. A dash means the key is absent from the JSON.
- AllocatorPreview renders any column with the same components the allocator's
browser uses, from the same projection, so the preview cannot lie.
- InvitePanel mints, lists and revokes links, with view counts and last-seen.
- Two more routes and still no router library; the regex is four lines.
Page chrome moves to shell.tsx and is shared by both sides, deliberately
related but not identical — the console adds an onyx rail, so a manager glancing
at a screenshot knows in a quarter second whether they are looking at their own
view or at what they published. Confusing those two is the mistake this product
exists to prevent.
The admin token lives in localStorage rather than a cookie: no cookie means no
ambient authority, so a link an allocator clicks can never carry the manager's
credentials with it.
Staged demo data moves to demo.ts alongside a fourth invite — the same fund as
the first, through the narrowest profile. Opening both side by side is the demo.
Also rewrites the public copy in a plainer voice.
Drops the opening summary sentence — the memo says what it is by saying it — swaps the two footnotes so the load-bearing caveat lands first, and dates it properly. Adds the rendered PDF so the memo can be handed over without a browser.
Nothing links to it, and it explained ground the build plan and README now cover in place.
Adds the section a judge or an allocator reads first: what the manager sees at /manage versus what the allocator gets from a link, with the measured byte-counts per profile in a table, and the note that a dash there means the key is absent from the JSON rather than hidden in the UI. Documents both demo doors and the admin token, spells out that demo_admin_token is served only under PODARENA_DEMO_FIXTURES, and updates the test count to 94.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub. |
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
A relative sqlite URL copied from a laptop resolves against the working directory, which is /app in the container and has no var/. SQLite will not create a missing directory — it says "unable to open database file" and does not say which file — so the deploy failed before the schema was applied. Bootstrap now prints the URL (password hidden) before it connects, because the failure this most often hits is that the URL is not the one you think it is.
…anager The console had one "admin" credential doing two jobs. A pod shop has two people inside the wall: the fund, which allocates across pods and sees the roster, and a PM, who runs one book. Collapsing them was less code and a lie — this product is about who can see what. - PM tokens are pm_<slug>_<mac>, HMAC-derived from the fund's token. No new table, deterministic across a reseed, and knowing your own tells you nothing about the pod next door. - Per-record console routes take ScopeDep; roster-level ones take FundDep. A PM asking after another pod gets 404, not 403 — a refusal that distinguishes "not yours" from "no such thing" leaks the roster one guess at a time. - /api/me filters the roster server-side, so a PM's browser never receives the other names to hide. - Sign-in is now one button per seat, and the buttons carry the argument: "Fund · all 3 pods" next to "Meridian · this pod only". - One rail colour per seat, so a screenshot says whose eyes you're looking through before you read a word. 99 tests, five of them new and negative. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing to copy from anywhere — the fund's token is already in the field, so the whole login is one press of Go, or one click on a seat. Typing takes over the moment you touch the box, and on a real instance there is nothing to prefill so it starts empty. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…demo The front page opened onto three doors and assumed you already knew why you were picking one. Now the hero carries the argument and a single CTA straight into an allocator's view of Meridian, and a three-step strip underneath says what actually happens: forward the statement, we check the signature and write the DNS key down on Monad, the allocator verifies it without us. Three steps because there are three, not because three looks good on a landing page. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It read like a document: a wall of hero prose, then cards of paragraphs explaining the idea to someone who had already decided to care. Nobody decides to care while reading paragraph four. Five sections now, each making one point and handing off to the next: the problem stated as the three things an allocator can actually do (two of which fail), the pipeline as four steps, the Meridian/Northwind pair as one picture (same +21.89%, twelve marks, one of them dashed), and a seat picker that ends on the demo links. The page also moves for the first time. Sections fade up as you reach them, the pipeline's connectors run in the direction the evidence travels, and the hero card checks a DKIM signature on a seven-second loop, which is the whole product in six lines. It is theatre over a page that reads fine standing still, so prefers-reduced-motion turns all of it off without changing a word. Two CTAs rather than one, because "sign in as a fund or a PM" is a door the old page buried below three screens of prose. Copy is rewritten without em dashes throughout.
The rebuild still read like a memo with pictures next to it. Every tile carried three sentences explaining an idea the tile had already made. Body copy is now one line per card. The visuals take the weight: line-art glyphs for the four pipeline steps, drawn inline for the same reason the charts are, since an icon font is a network dependency on a page whose argument is that it needs nobody else's server. The two failing options now look like they failed, with no fill and a dashed edge, instead of saying so in a paragraph. Also cut, all of it text that was explaining rather than showing: - the hero's "everything is pre loaded" note - the seat picker's heading, since three labelled doors are the heading - the PM rail's lecture about the roster next door - the footer's "the signature was in your inbox the whole time" - the landing footnote that narrated its own missing registry, which is now simply absent when there is no address to link to Northwind's strategy field said "Same numbers, one month short of proof", which is a note to the audience wearing a fund's clothes. It reads as a real strategy now, and the record page stops emitting a stray separator when a record has no strategy at all.
One stroke doing both jobs the product does: it starts as a check mark and leaves as an equity curve. Drawn by the same dawn arc as the wordmark, but stopped at the steel→cream half — the umber end is dark-on-dark against Onyx and is gone by 16px. svg for modern browsers, a 16/32/48 .ico for everything else (each size rendered from the vector, not downscaled from one), and a 180px touch icon with a square ground so iOS can apply its own corner mask.
web/tsconfig.tsbuildinfo is an incremental compile cache. It has no business in review — it rewrites itself on every build, so it turns up dirty in `git status` after anyone runs the web build and gets swept into unrelated commits.
`access_event` is append-only in Postgres and a trigger rejects UPDATE, so filling `chain_tx` in after the insert raises. Call `log_access` first and write the row complete. The ordering costs almost nothing: `log_access` never raises, it returns a failed receipt, so a network blip still writes the row with a null `chain_tx`. The only loss is a crash during the RPC itself.
The platform is the system of record for PM and fund statements, so arguing against "the bank's API" argued against something that isn't in the story. The rung we actually own is `platform_observed` — "We saw it. Our word only." Saying that out loud is what makes the DKIM card earn its tick.
Without a `branch:` key Render builds the repo's default branch. Ours is `main`, which is 18 commits behind and still contains the append-only bug fixed in f476baa — so the fix was committed, pushed and reviewed while the running server kept raising on it. Pinning the branch here says out loud which code is the deployed code.
The App row still said 'not yet deployed', which is the one sentence a judge checking eligibility reads as a failed gate. It now points at the deployment and at the staged allocator links. The strategy memo is internal — pricing, the ARR ramp, the list of likely acquirers, the size of the raise. The README was linking the whole room to it. The files are still in docs/; only the invitation is gone.
The 1..jpg name was a slip of the screenshot tool. This is the full-detail allocator view of meridian-global-macro, watermark and all.
Summary
Builds the manager's half of the product. Until now the app only had the
allocator's side: open a link, see a record, verify it. A manager could create
invites but had no way to see what those invites actually hand over.
The centrepiece is a disclosure matrix that measures rather than asserts. It
runs the projection once per profile and reports the serialized byte count and
the surviving top-level keys, so a dash in the table means the key is absent
from the JSON — not hidden in the UI. Clicking a column renders that exact
payload with the same components the allocator's browser uses, from the same
project()call, so the preview and the real thing cannot drift apart.Changes
API — four admin-guarded routes:
/owner(everything, no watermark),/disclosure(the measured matrix),/preview/{profile}(the exact bytes anallocator gets),
/invites(who holds a key, and what it opens). Extracts_projection_inputsfromview(), which was already assembling those fivequeries inline. 16 new tests.
Demo login —
/api/configserves the admin token so the console's loginscreen fills itself in.
NoneunlessPODARENA_DEMO_FIXTURESis on, and afixtures instance holds nothing but the synthetic corpus. Both halves tested.
.env.exampleandrender.yamlsay plainly what that means for a real deploy.Web —
/manageand/manage/:slug, still no router library. Page chromemoves to
shell.tsx, shared by both sides but visibly different: the consolegets an onyx rail so a manager can tell their own view from what they published
at a glance. Admin token in
localStorage, not a cookie — no ambient authority,so a link an allocator clicks can never carry the manager's credentials.
Visual system — the stylesheet is rebuilt on named tokens, and the trust
hierarchy is now weight, not hue: solid fill for signed evidence, then outline,
then quiet outline, then greyed. Chart points and badges use the same ranking;
deposits and withdrawals differ by dash pattern. A colour-blind allocator reads
the same ordering everyone else does.
Docs — README gains the two-sides section with the byte-count table and demo
credentials; strategy memo tightened and shipped as a PDF; the unlinked
blockchain primer is gone. Plus the
Pod Arena→PodShop Arenarename,isolated in its own commit.
How to test