Skip to content

Deploy to PROD 11/6/26 - #2351

Merged
Mephistic merged 7 commits into
prodfrom
main
Nov 6, 2026
Merged

Mephistic merged 7 commits into
prodfrom
main

Conversation

@Mephistic

Copy link
Copy Markdown
Collaborator

Changelog

Extra Deploy Steps

None.

fastfadingviolets and others added 7 commits November 4, 2026 20:21
Keep Share your input beside the conference title, greyed, once the reader has filed
* Show the reader's own input first in Public Input

The reader's filing appeared twice: in a Your Input section above Public
Input and again in the feed. The section goes. The feed now takes the
reader's entry out of the list and puts it first, outside the filters and
the five-per-page count, so no filter or page hides it. It is marked with a
soft brand background and a "You" chip. Posting still jumps to the card,
which now carries the #your-input anchor.

The highlight is built from existing tokens as a placeholder; the client's
designer has yet to see it.

Closes hyphacoop#125

Verified: jest components/conference passes, including new PublicInput
cases for the reader's card (first, once, on page 3, under an excluding
filter, alone). On the local emulator, posting landed on the card in
Public Input, with one copy, first on page 2 and no Your Input section;
a 375px-wide frame had no horizontal overflow. ESLint is clean on the
changed source files.

Not verified: yarn check-types, which fails repo-wide here on styled-components
and JSX types, lines this change did not touch included. The
highlight against the client's designer. A filter that excludes the reader,
in the browser.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Tidy Public Input after the reader's card moved in

The scroll-margin calc was written out twice in PublicInput, once for
the section and once for the reader's card, so the two could drift and
park a jump at different offsets. They now share one fragment. Two
comments in the tabs code still described the removed Your Input section.

Verified: jest on PublicInput and ConferenceTabs passes; Prettier is clean.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
… ones (#2345)

* Take the Sources list out of "What needs to be resolved?"

The section no longer ends in a numbered Sources list, and the points no
longer carry superscript marks referring to it. This matches the client's
mockup, which parks its Sources. The page stops loading sources: the view
types and the loader drop them, along with the two locale strings.

Each point's authored source stays in the YAML and on the conference
document, so authors keep recording where a point came from.

Closes hyphacoop#124

Verified: eslint and prettier are clean on the touched files; the conference
jest suites pass, including a test that no Sources label or superscript
renders.

Not verified: the section laid beside the pinned mockup at desktop and phone
widths. yarn check-types fails across the repo in files this change does not
touch, so it did not confirm the change.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Show the conference gist as a short paragraph

The gist is now a few sentences rather than one line, so it sits at the
answer cards' text size with a readable line length, and the feedback
invitation moves to its own fainter line beneath it. The gist comment and
the authoring doc say "short paragraph", and that a source is optional.

Verified: eslint on the component, prettier, conference jest suites.

Not verified: the page rendered, at desktop or phone width.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Write the partner's open and settled questions into nine committees

Each committee's gist, differences and similarities now come from the
media partner's 11/4 questions draft, replacing the lorem ipsum on
Phone-Free Schools and the empty lists elsewhere. Sources, notes and
section citations for settled points are left out. Chambers whose bill
doesn't touch a point are left out so the page shows "Not in the bill".
Two settled rows that were not matches are dropped. The other seven
committees are untouched.

Verified: all sixteen files pass the Conference runtype with no unknown
fields; prettier; row counts match the draft.

Not verified: the pages rendered; the draft is marked for editorial
review and flags readings a lawyer should check, such as the nuclear
description; not synced to any database.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Follow the conference report's history when guessing status

A conference report is a bill of its own, and the legislature records
the enactment and the governor's signature on that bill's history, not
on the pair it reconciles. findConferences read only the pair's history,
so every reported conference stopped at reportFiled even once it became
law. It now reads the report bill's history too, drafts the report as
reportBillId, and prints the chapter of the acts it became.

Verified: against malegislature.gov for the five reported conferences:
each history's "Reported by H5xxx" names its report bill, and each
report bill's history ends in "Signed by the Governor, Chapter N of the
Acts of 2026".
Not verified: the script was not run end to end against dev.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Mark conferences we don't cover, and the report that resolved enacted ones

We're trialling the conference pages with some committees. A conference's
YAML can now say `covered: false`: it stays listed and linked, its name
is muted with a "Not covered" label in the rail, picker, explorer table
and phone list, and its page opens with a notice that we're trialling
with a limited set and this isn't one of them. It takes no input: the
shared takesTestimony gate now requires coverage, so the page, the
testimony page's actions and publishTestimony all refuse it. The landing
opens on the first covered conference. Mass Ready and BRIGHT, the
borrowing bills, are marked not covered.

A conference's YAML can also name its report bill (`reportBillId`); its
page then says "Resolved by H.5350", linked to that bill. The five
reported conferences are enacted, so their status moves from reportFiled
to enacted, each with its report bill. The sync refuses a report bill
on an appointed conference or one that is a bill of the pair, and warns
when the report bill hasn't been fetched.

Public Input and its tab now show only on covered conferences that take
input, or that got some before closing, read from the conference's
testimony count so a closed conference without input never shows it.

Both fields default (covered true, no report), so YAML without them
syncs unchanged; website readers don't validate documents, so a document
synced before these fields reads as covered.

Closes hyphacoop#123

Verified: type-check; components/conference jest (62), test:website
(159) and functions tests (145) pass; the conference sync integration
suite (13) and the uncovered-conference refusal in testimony.test.ts
pass on throwaway emulators; all 16 YAMLs validate against the runtype;
on a seeded local stack every conference page and H5350 return 200 with
the expected covered, reportBillId and testimony count props, and the
landing opens on a covered conference.
Not verified: the greyed rows, notices and hidden Public Input laid
beside the pinned mockup at desktop and phone widths; five cosign tally
tests in testimony.test.ts fail locally when the whole file runs, as
they do on the base commit without this change, and pass alone and in
CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Seed report bills and the sample conference's counts locally

seedLocal fetched each conference's two bills but not its report bill,
so a "Resolved by" link led to a missing bill page on a seeded emulator.
It now fetches the report bill too.

It also writes its sample testimony straight to Firestore, skipping
publishTestimony, which keeps a conference's testimony counts, so the
sample conference showed no input in its count. It now sets the counts
with the same updateTestimonyCounts the publish path uses.

Verified: on the local stack the seed fetched all five report bills
(the sync then warned about none); a hand-written version of the count
update set the sample conference's count to 7; the final form
type-checks.
Not verified: the final updateTestimonyCounts form was not re-run
against the emulator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…2319)

* Say why a cosign was refused, and refuse a legislator's testimony

The cosign panel tells a reader that the input they are cosigning has been
removed, or that the conference has stopped taking input. Both refusals are
failed-precondition, and a gone parent shares not-found with a gone draft,
so the error code cannot tell them apart. fail() now takes details, and
publish sends { refusal: "cosignParentGone" | "conferenceClosed" } with
those two refusals.

The rules for what can be cosigned move to a module with no dependencies,
so the website offers Cosign by the rule that publishing keeps: an
individual's or an organization's testimony, never a legislator's, a cosign,
or one's own. A legislator's testimony was cosignable before. The draft's
cosign shape lives there too, so the website's draft type can share it.

Verified: yarn test:integration-ci on the six suites CI runs (97 pass,
including the new legislator refusal and the gone-parent refusal's
details); yarn --cwd functions jest src (138); functions tsc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drop a draft or publication deleted elsewhere from the edit hook

The testimony edit hook ignored a watched draft or publication that stopped
existing, so a page kept what another tab, or now a cosign's cascade, had
deleted: the conference panel reopened as an edit of a gone publication,
asking for an edit reason, and saves failed against the deleted draft. A
snapshot that the server, not the local cache, reports gone now goes
through the hook's own delete and discard.

Verified: on the local emulators, deleting the signed-in user's cosign and
draft behind the open conference page, then filing plain input from Share
without an edit reason, which posted.
Not verified: the bill and ballot question pages, which share the hook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Cosign from a Public Input card, to the client's mockup

A reader can cosign an individual's or an organization's input on a
conference from its card, or from the card opened whole. The panel fills
the window with the input being signed beside it: the reader says they
have read it, then adds an optional note or says they are adding none, and
lands in Your Input, which names what they cosigned. Editing a posted
cosign changes its note only, with a reason. A signed-out reader goes to
sign in; Cosign is offered only while the reader has no input of their own
here, posted or drafted; and the two refusals the server words are shown
in the panel. The share-with-the-committee choice in the mockup is left
for its own issue, since nothing reads it yet. The letter sits in the well
our bill text uses, lighter than the mockup's grey.

Both forms share one draft hook held by the page, and its autosave, so
switching between them never files two drafts, and a quick post no longer
saves twice. Below lg the panel is now as wide as the window, where a
narrow phone used to cut off the plain form's edge.

Closes hyphacoop#97

Verified: on the local emulators (seedLocal), beside the pinned mockup at
about 1280px and 390px: the card's Cosign, both steps including the
unticked Continue and the empty-note ask, filing with and without a note
(the parent's tally went from 3 to 4), the landing, editing a cosign,
deleting the parent and closing the conference mid-cosign, a signed-out
Cosign opening sign-in, and plain Share, Review and Post unchanged;
npx jest components/conference (new tests pass; load.test.ts fails as it
does on main); tsc, prettier and lint.
Not verified: the edit-cosign view, which the mockup does not draw, by the
designer; widths other than about 1280px and 390px; the dev site.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Seed cosigns on the local sample conference

The local seed adds four cosigns of its sample testimony, with and without
notes, from cosigners with districts, so the cosign panel and Your Input can
be laid beside the mockup with a parent that already has a count. The
functions emulator's trigger keeps each parent's tally, as it would on dev.
Every seeded testimony now carries firstPublishedAt, as publishing sets it.

Verified: yarn firebase-admin run-script seedLocal --env local on fresh
emulators, after which the Massachusetts Medical Society sample carried
cosignCount 3 from 2 Senate districts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Size a Public Input card's portrait, marks and chip down on phones

The client's mockup brings the card's furniture down with its type: the
portrait to 34px under 767px and 26px under 450px, the account marks to
14px and 13px, and the position chip to 10px with less padding under
450px. Ours kept the desktop sizes at every width. The sizing sits on
the card's own portrait rather than the shared disc, so the cosign
panel's portraits keep theirs.

Verified: computed sizes in a 390px frame beside the mockup at
k-g-k/maple-conference@19bd101: portrait 26px with 10px letters, marks
13px, chip 10px with 6px padding; desktop unchanged at 40px.
Not verified: the 767px tier, between phone and desktop widths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Group cosigns under what they cosign in Public Input

A cosigned card now carries the mockup's band ("N constituents have
cosigned this", "including you" with a way to your own input, and the
Cosign button) and its floor (the count, the Senate and House districts
cosigners come from, and the last-cosigned date). Its cosigns are listed
under it, a tier quieter than the card, newest first, three shown and
the rest behind "Show N more cosigns".

This departs from the mockup's flat feed, where each cosign is a card
at its own date and noteless ones trail at the end: a card now ranks at
its latest cosign, so cosigning brings it back up, and its cosigns go
with it. Filters and paging take a card and its cosigns as one unit.
Cosigns without a note are listed only with "Show cosigns without
content" ticked, and counted either way. The local seed gains two more
noted cosigns so a card shows the cap.

Closes hyphacoop#98

Verified: band and floor beside the mockup at
k-g-k/maple-conference@19bd101 by computed style at desktop, and in a
390px frame; on local emulators, cosigning a card as the seeded test
user brought it to the top with "including you", its cosign listed
under it, and View input landed on Your Input; the noteless setting,
the expander and the badge count in the browser; components/conference
jest (the grouped feed's ordering, units, filters, paging, cap, noteless
setting and districts).
Not verified: the client's review of the grouped list, which is our
design; the 767px tier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep the last-cosigned date on a wide card in a narrow window

At 900px and under, the floor hid its "Last cosigned" date so the card's
foot line could carry it, but only a narrow card has a foot line. A card
600px or wider in such a window (no rail, ~700px) showed the date nowhere.
Entry now hands the floor the card's width, as it does its cosigns, and the
floor hides the date only on a narrow card.

Verified: npx jest components/conference passes apart from load.test.ts,
which fails on main from a stale fixture.
Not verified: the date by eye on a wide card in a narrow window, or at phone
width.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep conference drafts saving through failures and apart from cosigns

A failed save left the Post or Cosign button disabled for good and a timed
autosave stuck on "Saving". A post pressed while the edit hook was still
loading the draft had its save dropped and waited forever; it now saves
again once the draft settles. A flush made while a write is in flight no
longer starts a second one, so a quick Agree then Cosign can't add two
drafts. Delete Draft comes back when the fields return to what is kept.

The compose form, which stays mounted behind a closed panel, no longer
takes a cosign draft for its own: it does not seed from one, and its draft
check refuses one. Before, it could publish a cosign draft as a cosign, or
once refusing them, autosave a cosign's note as the reader's own input
after every page load.

A cosign posted without words reopens with "no words" only when no note
has been drafted since, so that note is not hidden and wiped. Signing out
clears the page's record of the draft, so a signed-out visitor sees Cosign.

Verified: npx jest components/conference passes apart from load.test.ts
(stale fixture, fails on main); InputForm.test fails without the seeding
fix. On local emulators, a cosign draft with a note survived a reload as a
cosign. yarn check-types clean in the changed files.
Not verified: failed saves and the dropped-save post by hand; sign-out with
a draft.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Warn before a cosign replaces a note drafted for another

The reader has one draft on a conference. With a note drafted cosigning one
input, agreeing to cosign another wrote over that note without a word. Cosign
stays on every card, but the agreeing step now says that continuing deletes
the note on the other cosign, naming it when it is still in the feed.

Verified: npx jest components/conference passes apart from load.test.ts
(stale fixture, fails on main), with tests for warning and for staying
quiet over this parent's draft or one without words. On local emulators:
drafted a note cosigning Fight for the Future, reloaded, pressed Cosign on
Massachusetts Medical Society, and the warning named Fight for the Future.
Not verified: the warning at phone width; the wording with the client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drop the district counts from a cosigned card's floor

The floor read "N cosigned · X Senate · Y House"; the last two counted
the distinct districts the cosigners live in, which confused and weren't
needed. It now shows the cosign count and the last-cosigned date. The
tally still writes the two district counts; nothing reads them.

Closes hyphacoop#127

Verified: npx jest components/conference/input passes, including a card
that carries district counts showing neither label.

Not verified: the floor by eye at desktop and phone widths. Dropping the
district counts from the tally, the spec's FR-1 and #92's brief waits on
the data-model decision.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Fix the React 19 type errors that failed the Vercel build

The cosign commit passed a RefObject<HTMLTextAreaElement | null> to a
parameter typed RefObject<HTMLElement>, called useRef with no argument, and
left an onChange parameter untyped where styled(Field) drops the event type.
next build type-checks these; the repo's CI does not, so only the Vercel
deploy failed.

Verified: yarn check-types is clean outside the untracked scripts/firebase-admin/_*.ts; npx jest components/conference/input (36 pass); prettier clean.

Not verified: a full yarn build:dev, which still stops on an untracked scratch script that Vercel never sees; the Vercel deploy itself.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Put the reader's own cosign first, under the card it cosigns

A reader whose input is a cosign now has the card they cosigned pinned
first in Public Input, outside filters and paging, with their cosign
first in the group under it, tinted and marked "You" as their own card
is. Their cosign shows even without a note and past the three-row cap,
and it carries the your-input anchor, so posting and the band's "View
input" link land on it. A cosign whose card is not in the feed still
stands alone, as before.

Closes hyphacoop#129

Verified: components/conference/input and ConferenceDetails jest suites
pass; prettier clean; check-types clean outside scripts/.

Not verified: the row beside #125's own card at desktop and 390px; the
post-and-land jump in a browser; yarn build:dev.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Share one keyboard-usable sortable header across the lobbying tables

The Bills, Clients and Lobbyists lists and the bill page each had their own
SortTh. They now share components/lobbying/SortTh.tsx, along with the header
style. The header's label is an unstyled button, so it can be reached with Tab
and sorted with Enter or Space, and the sorted column has aria-sort. Every
sortable column shows an arrow (↕ until it is sorted), so headers no longer
change width when the sort changes; only the bill page did that before.

Verified: npx jest components/lobbying (SortTh.test.tsx: keyboard sort,
aria-sort, ↕ on unsorted columns); tsc --noEmit shows no errors in the files
changed; prettier --check; next lint adds no warnings.
Not verified: sorting on the four pages in a browser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Add the Lobbying Explorer redesign's shared activity helpers and components

The redesigned Bill and Client pages group filings by organization or by bill,
per half-year reporting period, and show a position bar and an expandable
"who lobbied" table. This adds those pieces, tested, for the page rebuild in
#90; no page uses them yet.

- activity.ts: normalizePosition, now the only one, follows the scraper's rule,
  so the precomputed counts and the pages agree. Existing pages now show
  "Monitor" filings as Neutral. periodOf takes the half-year from the filing,
  then its registrant, then falls back to the year. groupActivity builds the
  rows, with in-house status per client and a firm's agents on that client;
  orgPositionBreakdown counts each organization once, by its latest period.
- PositionBar: support/oppose/neutral in the MAPLE position colors, plus a new
  purple for "multiple positions", with counts on hover, in its label and in
  an optional legend.
- ActivityTable: rows open with a button to each filer's position per period
  and SoS filing links; it scrolls inside its box on narrow screens.
- fetchRegistrantsByIds fetches registrants in batches of 30. The
  lobbyingClients hooks, which read a collection nothing writes, are removed.

Closes hyphacoop#89

Verified: npx jest components/lobbying (37 tests, incl. mixed positions
across periods, a filer with no period data, an org in-house for one client
and a firm for another, opening a row by keyboard); tsc --noEmit shows no
errors in the files changed; prettier --check; next lint adds no warnings.
Not verified: PositionBar and ActivityTable in a browser or at phone width
(no page uses them until #90); fetchRegistrantsByIds against Firestore;
Monitor filings showing as Neutral on a live page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Rebuild the Lobbying Explorer pages to the redesign

The six Lobbying Explorer pages now follow the redesign canvas's layout,
labels and interactions in MAPLE's colors.

- Overview: key figures per session, a payments-by-session chart whose bars
  pick the session, and the most-lobbied bills (overall for all sessions).
- Bills and Clients lists: the redesigned columns (organizations with
  position bars; bills, lobbyists split into in-house and firms with their
  agents, and reported paid).
- Bill and Client pages: a summary and an expandable organization or bill
  table, sortable and filtered by position.
- Lobbyists list: the shared table style and a count.

Every session select opens on, and marks, the session in progress.

To keep pages light, the Overview and the Client page are prerendered with
their data. The Clients list embeds only the latest session and loads others
through a CDN-cached API route; all regenerate daily. A Client page reads
only the selected session's filings. Firestore's "client is offline" error
reads as the server being unavailable.

Closes hyphacoop#90

Verified: npx jest components/lobbying (59 passed), tsc --noEmit (only the
4 typesense errors from local node_modules), next lint, prettier --check.
In the browser, against a full local scrape in the emulator: H4746's 105
organizations and 250 filings, the Greater Boston Chamber of Commerce's
194th summary, the 194th's 5,650 bills, sorting both ways, session and
position filters, picking a session from the chart, keyboard expand and
row clicks, fixed column widths through re-sorts, and phone width. That
also covers the checks 5e14038, hyphacoop#89 and 06b1902 left open
(the sortable headers, PositionBar and ActivityTable in a browser and at
phone width, and the Clients page showing inHouseLobbyists and hiredFirms).

Not verified: next build and static regeneration (the local build stops on
the typesense type error); the pages on dev or prod, which need the #106
deploy; Firefox's session select width (checked in Chromium only); All
sessions for the largest clients' pages (the emulator rejects the ~25 MB
response).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Use the redesign's position colors in the Lobbying Explorer

Positions are now blue for support, orange for oppose and grey for neutral,
as on the redesign canvas, in place of MAPLE's green, orange and navy. One
table in chartTheme holds each position's bar, text, chip and square
colors. Position bars, legends, the Bill page's counts, the chips in the
tables and the lobbying card on MAPLE's bill pages all read from it. Text
colors are all 4.5:1 or better on their backgrounds. The canvas's neutral
grey is 2.1:1 as a bar segment; each segment's count is also in its hover
text and the bar's label.

Verified: npx jest components/lobbying, tsc --noEmit (only the 4 local
typesense errors), next lint, prettier --check. In the browser on the full
local scrape: the Overview's top bills, H4746's summary bar and counts,
and the chips in its organization table.

Not verified: the lobbying card on MAPLE's bill pages in a browser (the
local bill pages 404 without bill data).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Show every report period of the session in expanded activity rows

An expanded organization (Bill page) or bill (Client page) now lists every
report period of its session that has ended, newest first, for each filer.
Each period is a lettered square in the position's color (S, O, N), or an
empty dashed square when the filer didn't file that period, as on the
redesign canvas. The periods used to be only those the row's own filers
filed in. A new period's lane stays empty until its reports come in.
sessionPeriods and sessionPeriodsWith in activity.ts build the list, and
each period carries a short label for the captions.

Verified: npx jest components/lobbying (new tests for the period list and
the empty squares), tsc --noEmit (only the 4 local typesense errors), next
lint, prettier --check. In the browser on the full local scrape: H4746's
ACLU and Abbott rows show Jan–Jun 26, Jul–Dec 25 and Jan–Jun 25 with
filled and empty squares.

Not verified: a Client page's All sessions view, where each bill row
shows its own session's periods.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Match the redesign's banner and Bill page title, and sort bills by organizations only

- Banner: the Lobbying Explorer's beta banner is one centered sentence with
  the email inline, as on the canvas. The shared Banner is a flex row,
  which broke it into three lines; the lobbying banner now uses Bootstrap's
  theme classes on a paragraph, and the other banners are unchanged.
- Bill page title: it stays the page's h1 at h3 size, since a bill's title
  can run long. The Client page's name keeps the full h1 size.
- Bills list: only the Organizations column sorts.

Verified: tsc --noEmit (only the 4 local typesense errors), next lint,
prettier --check. In the browser on the full local scrape: the banner on
one line at 1280px and wrapping at 390px; H4001's title at 30px (24px on a
phone); the Bills list sorting by organizations both ways with a plain
Bill header.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Use the scraper's precomputed aggregates in the Lobbying Explorer pages

The pages now read what compute_stats precomputes on fix/88 instead of
working it out in the browser:

- The Bills list and the Overview's most-lobbied bills draw each bar from
  every organization counted once (orgLatest*), with a "multiple" segment,
  so they match the Bill page.
- The Bill and Client pages leave out the "No client" placeholder and H0/S0
  rows, by the scraper's own rules.
- Lobbyist totals from the 189th session on are exact: individuals plus
  hired firms that named no agents.
- The Client page takes its in-house filers from its summary.
- The Clients list reads one precomputed doc per session.
- The Lobbyists list now loads per session like the other lists. It
  embeds only the current session and loads others through a new cached
  /api/lobbying/firms/[session] route, so its page data drops from
  2.29 MB to 514 kB.

Along the way, every lobbying data hook reports a result only for its
current arguments, the three list API routes share one handler, and bill
rows carry only the fields the lists show.

Verified: npx jest components/lobbying (65 passed), tsc --noEmit (only the
4 local typesense errors), next lint, prettier --check. In the browser on
the full local scrape rebuilt with fix/88: H4746 8/23/67/7 and H4001
100/5/266/13 on the Bill page, Bills list and Overview alike, with no "No
client" rows; H1 first in the 194th; Abbott Laboratories "1 in-house · 1
firm (3 agents)"; the Greater Boston Chamber of Commerce's 39 bills, 6
in-house lobbyists and its own SoS page; Lobbyists 2,164 (194th), 2,009
(191st) and 8,146 (all); session switches show loading, then the new
rows. That covers the check 369d878 left open (the #90 pages reading the
new fields).

Not verified: next build and static regeneration (the local build stops
on the typesense type error); the CDN caching of the new clients and
firms routes; the pages on dev or prod, which need the #106 stats run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* List "No client" on the Lobbying Explorer pages, not as an organization

The scraper lists the portal's "No client" again, so readers can see
lobbyists who reported no client, but still leaves it out of client and
organization counts. The pages now do the same:

- The Bill page keeps its "No client" row, tagged "Not an organization",
  and leaves it out of the organization count and the position bar, so
  they still match the stored counts. Its filings and filers still count.
- The Clients list sorts it in with the rest, tagged, with only its
  people count under Lobbyists, and leaves it out of "N clients".
- Its own Client page says it isn't an organization in place of the
  registration line and SoS link, and drops the in-house clause.
- Filers of "No client" show no in-house or firm label, and the
  Lobbyist page tags it in a lobbyist's client list.

The legacy total-salary row is still dropped everywhere.

Verified: npx jest components/lobbying (66 passed), tsc --noEmit (only
the 4 local typesense errors), next lint, prettier --check. In the
browser on the local scrape rebuilt with d5d8fab: H4576 (194th) shows 1
organization and an oppose-only bar, plus the "No client" row (Gregory P
Vasil, Jackie McCarthy); H4746 still 8/23/67/7 and H1 413 organizations;
the 194th Clients list still 1,810 clients, with "No client" at 48 bills,
23 lobbyists and $81,725; its Client page and Jackie McCarthy's page as
described.

Not verified: the pages on dev or prod, which need the #106 stats run to
write the placeholder flag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Line up expanded activity rows with the table's columns

As in the redesign, an expanded row's filer box now sits under the Most
recent position, Who lobbied and Filings columns, not across the whole
row. Each filer's period squares line up with the position chips, and
its name, kind and filing links with the Who lobbied text. The position
column widens from 180 to 300 px, the redesign's minimum, so a session's
three period squares fit on one line.

Verified: in the browser on the local scrape, on H4746 (194th) and
Abbott Laboratories' Client page: squares at 453 px under chips at
452 px, filer names at 752 px under the Who lobbied text at 752 px;
prettier --check.

Not verified: phone width, where the table now scrolls from 840 px
instead of 720 px.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Label report periods the same way on every Lobbying Explorer page

Period labels came from three places: the filings tables' formatter
("Jan–Jun 2025"), the activity rows' own month strings, and the Lobbyist
page's date ranges ("1/1/2026 – 6/30/2026"), so that page labeled the
same period two ways. All of them now use the filings tables' formatter,
which gains a short-year form ("Jan–Jun 26") for the period squares.

Verified: npx jest components/lobbying (71 passed), tsc --noEmit (only
the 4 local typesense errors), next lint. In the browser on the local
scrape: Jackie McCarthy's Lobbyist page reads "Jan–Jun 2026: $9,542" and
the like with no date ranges left; H4746's period squares read
"JAN–JUN 26" with "Jul–Dec 2025: Support" on hover; no page errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep the lobbying list docs under Firestore's index entry limit

Firestore indexes every field by default and allows 40,000 index entries
per document, a limit the emulator doesn't enforce. The per-session
Lobbyists and Clients list docs held one indexed field per row, so the
dev stats run failed writing firmList_192, and firmList_193 and
clientList_all would have failed after it.

The list docs now hold their rows under one "rows" map, which
firestore.indexes.json exempts from indexing; nothing queries these
docs, each is read whole by id. The stats run's pre-write check now
estimates each document's index entries next to its size, stops before
writing anything if one passes the limit, and logs each doc's count.
The filing counts and inHouseFilers docs, which the current pages read,
keep their shape: the largest is about 10,600 entries.

Readers of these docs must read rows from "rows"; only the unmerged
redesigned list pages do.

Verified: pytest in lobbying-scraper (200 passed), including the
estimate, the stop before writing, and a check that the exempt field is
exempt in firestore.indexes.json. On a copy of the local emulator data,
the estimate puts firmList_192 (42,979), firmList_193 (40,266) and
clientList_all (55,722) over the limit and every clientList_{gc} under,
matching what dev wrote and refused. A full stats rebuild against that
copy wrote every list doc under "rows" with no errors.

Not verified: the stats run on dev once the exemption is deployed; that
Firestore applies the exemption to the rows' subfields as documented.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Give the lobbying scraper job enough memory for the stats step

The stats step ran out of memory on dev at Cloud Run's 512 MiB default.
deploy.sh now creates and updates the job with 2 GiB and 1 vCPU, about
three times the measured peak, so a recreated job or the prod job don't
fall back to the default.

Verified: a full local stats rebuild (592,149 filings) peaked at 688 MiB
resident memory.

Not verified: deploy.sh against a project; the job's peak memory on
Cloud Run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Read the lobbying list rows from their rows field

The stats run now writes the Lobbyists and Clients list docs as
{rows: {normalized name: row}}, so their rows stay out of Firestore's
index entry limit. fetchFirmList and fetchClientList read the rows from
there; every list page and list API route goes through them.

Verified: against the local emulator rebuilt with the merged scraper,
the firms and clients API routes return 2,164 (194), 2,241 (192), 1,811
(194) and 5,572 (all) rows, matching the stats log, and the Lobbyists
and Clients pages render with their current session's rows.

Not verified: the pages in a browser or at phone width; against dev,
which has the flat list docs until its stats run succeeds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fetch each summary page once per scraper run

process_disclosure re-fetched the registrant's summary page for every
disclosure, so a registrant with N disclosures cost 1 + 2N requests.
It now takes the parsed summary from its caller. The backfill passes the
one it already holds; the weekly run passes the one it fetched live, or,
for a prior year served from the summary cache, fetches it once when the
first unprocessed disclosure needs it (none when nothing is new).

Closes hyphacoop#101

Verified: backfill from the local archive into empty emulators, old vs
new code, one worker: 2016 (20 registrants) summary fetches 59 -> 20,
2011 (1,820 registrants) 4,517 -> 1,820, disclosure fetches unchanged;
all 1,064 and 6,749 documents identical apart from fetchedAt and the
order of the clients list, which also varies between two runs of the old
code. Live weekly run, 2026 limit 5: summary GETs 10 -> 5, 152 documents
identical. Scraper unit tests pass, including new per-registrant fetch
counts.

Also with 8 workers, full 2011 from the archive: two runs of the old code
and one of the new write 6,749 identical documents (clients list order
ignored), summary fetches 4,592 / 4,586 -> 1,820.

Not verified: the weekly prior-year cache-hit path against the live
portal (unit tests only).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Center the key figure numbers between their row lines

Each key figure row lined its number up with the first line of its label,
so a wrapped label, or the Lobbyists row's line about firms, left far more
space below the number than above it. The row now centers the number, its
label and the info button, and the number's line box hugs its digits.

Closes hyphacoop#120

Verified: against dev data in Chromium at 1280 and 375 px, the space
above and below each number went from 16/33, 16/18, 17/65 and 14/33 px to
20/20, 15/15, 37/36 and 18/19 px; an opened definition still lines up
under its label at both widths.

Not verified: the All sessions view, where the numbers are wider; Firefox
and Safari.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep a lobbying client who was paid nothing

The 2014-2018 layout (also used by recent individual disclosures that report
no activity-level compensation) rebuilds each client's amount from its
"Total amount paid by client" panel and dropped any client at $0. A
lobbyist's $0 client then vanished from "Clients represented". The legacy
2009-2013 path had the same filter.

A client with a recorded $0 now keeps its compensation entry. Fixture totals
are unchanged; the 2011 and 2016 fixtures gain their $0 clients (3 and 1).

Verified: scraper tests pass; the new test fails without the fix. Parsing
Adam Romanow's live Jul-Dec 2025 disclosure took the Hybrid path and dropped
Massachusetts Brewers Guild before the change.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Count each session's clients from the client list

The overview's per-session client count came from filings, so a client named
on a disclosure's compensation but with no activity rows was in the Clients
list and not in the count (1,759 against 1,810 on dev for the 194th). Both now
come from the client summaries, less the "No client" placeholder.

Verified: scraper tests pass; the new test fails without the change. After a
rebuild from the local archive into a fresh emulator, every session from 184
to 194 has stats clients equal to its list rows minus the placeholder, and
clientList_all matches totalClients.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Record the agency on Executive lobbying filings

An Executive activity has no bill number; the portal's identifier cell names
the agency (for example "Office of the Governor"), but the filing doc dropped
it, leaving the row with only a title. Filing docs now carry `agency` for
Executive items and null for bills. Doc ids are unchanged, so existing docs
are overwritten in place by a rewrite from the archive.

Closes hyphacoop#121

Verified: scraper tests pass; the new test fails without the change. In the
local archive rebuild, Adam Romanow's 5 Executive filings carry agency and no
bill row does. His 2025 rows read "Office of the Governor"; his 2024 rows read
"Other", the portal's own text.

Not verified: dev. Its docs stay as they were until the migration's write
phase and a stats run are done there.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Show non-bill lobbying activity instead of a dash

A lobbyist's activity table showed "—" under Bills for items with no bill,
such as Executive items like a chapter of the Acts, even when a position was
reported on them. The cell now names the item, "Executive" (with the agency
when the portal names one, e.g. "Executive · Office of the Governor") or
"Other", with its title beneath. The portal's literal "Other" agency is not
shown. On a lobbyist's page these rows now sort after the year's bills
instead of first.

Verified: yarn test:website; in yarn dev:local against an emulator rebuilt
from the archive, Adam Romanow's page at 1280 and 375 px shows his 2025 rows
as "Executive · Office of the Governor" with the title, his 2024 rows as
"Executive" with the title, each after the year's bills.

Not verified: on dev, where filings carry no agency until the migration's
write phase runs; rows show "Executive" and the title until then.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Call lobbying rows activities, not filings

Each row counted as a "filing" is one activity on a disclosure (a bill or
an Executive item with a position), so a lobbyist with 5 disclosures showed
"87 filings". Counts, column headings and empty-table text now say
"activities", the portal's own name for these rows; "filing" and
"disclosure" keep meaning a whole report. Only the wording changes: keys and
code identifiers stay. Drops billCard.filingCount_*, which nothing used.

Verified: yarn test:website; in yarn dev:local, Adam Romanow's page reads
"87 activities", the Lobbyists list heads its column "Activities", and a
bill page reads "…in 867 activities by 299 lobbyists and firms."

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Explain "—" and "0" in the Lobbyists list

A note under the list says what "—" and "0" mean in Clients represented and
Activities, and a "—" cell carries a short tooltip.

Verified: in yarn dev:local at 1280 px the note shows under the list.

Not verified: the note's meaning in a session view. That view still shows
"—" for a lobbyist who filed empty disclosures that session (e.g. Brian
Delburn in the 194th), where the note says "0"; a per-session flag from the
scraper's stats fixes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Mark who filed a disclosure in each session on the Lobbyists lists

A session's Lobbyists list showed "—" for anyone with no clients and no
activity that session, including a lobbyist who filed disclosures of their
own that reported nothing. The list can't then tell "registered, but an
entity files for them" from "filed, but reported no clients". Each row of a
session's list now carries filedInSession: whether the lobbyist or entity
filed a disclosure of its own that session.

Verified: scraper tests pass; the new test fails without the change. After
a stats rerun on the emulator rebuilt from the archive, Brian Delburn's 194th
row has filedInSession true with 0 clients and 0 activities; rows without it
drop from 921 to 486 in the 194th (642 to 117 in the 184th), and no row has
clients or activities without it.

Not verified: dev, until its stats run after deploy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Show a session's counts for anyone who filed in it

A session's Lobbyists list showed "—" for anyone with no clients and no
activities that session, including a lobbyist who filed disclosures of
their own reporting nothing, which the note under the list says shows as
0. The list now reads the stats' filedInSession flag: filed that session,
the counts show (0 when empty); registered only, "—". Rows written before
the flag fall back to the old rule. In All sessions, one that filed but
never reported an activity shows 0 activities rather than "—". The note
now says "in the sessions shown".

Closes hyphacoop#122

Verified: yarn test:website; in yarn dev:local against the emulator with
the new stats, the 194th view shows Brian Delburn as 0 / 0 (was — / —) at
1280 and 375 px, and a registered-only lobbyist (Alexander Furey) keeps —.

Not verified: dev, which keeps the old rule until its stats run writes
filedInSession; the All sessions view in a browser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Label a client's SoS registration link with its year

A client page's "reported paid" covers the session, but its SoS
registration link opens one year's registration page, so 2U Inc. showed
$21,250 for the 194th (2025 and 2026) and a link to a page listing $10,000
(2026 only). The link now reads "2026 SoS registration", from the client's
last registered year; without one it keeps the plain label.

Verified: yarn test:website; in the emulator data the linked page's year
equals the last registered year for all 5,762 clients with a link; in
yarn dev:local, 2U's page reads "2026 SoS registration" at 1280 and 375 px.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Put the session's years on their own line in Key figures

The Overview's Key figures heading read "Key figures · 194th General
Court (2025–26)". The session stays in the heading and its years move to
a second, lighter line in full ("2025–2026"). For All sessions the line
gives the span of sessions with data ("2005–2026").

Verified: screenshots of the panel at 1280 and 375 px for the 194th, and
at 1280 px for All sessions, against yarn dev; yarn test:website passes;
check-types shows only the 4 known errors; no new lint warnings.
Not verified: other sessions in the select, beyond the shared helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Label the payments chart's bars with their years

The Overview's payments chart named each bar by its session ("194th").
It now gives the session's years ("2025–26"); the tooltip keeps the full
"194th General Court (2025–26)". At phone width the chart labels every
third bar, which is fine as it is.

Verified: screenshots of the chart at 1280 and 375 px against yarn dev;
check-types shows only the 4 known errors; prettier passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Show a position bar segment's count as soon as it is hovered

Each segment of the position bar carried its count in a title
attribute, which the browser shows only after about a second. The
segments now use react-bootstrap's tooltip, which shows at once (its
default delay is 0), above the segment. Screen readers still read every
count from the bar's label. This covers the bar on the Overview's top
bills, the Bills list and the bill page; the bill card on MAPLE's bill
pages draws its own bar and keeps its title.

Verified: yarn test:website passes; check-types shows only the 4 known
errors; prettier passes.
Not verified: the tooltip in a browser, on any page; tapping a segment
on a phone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Set Key figures' years in the heading's own font

The years under the Key figures heading ("2025–2026") were lighter and
gray. They now take the heading's weight, spacing and color, and keep
only their own line.

Verified: screenshot of the panel at 1280 px against yarn dev;
check-types shows only the 4 known errors; prettier passes. In the same
session, hovering a position bar segment on the Overview showed its
tooltip ("Support: 133 organizations") in about 70 ms, which 5112f0a
lists as not verified.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drop the sort hint from the Bills list

The Bills list's panel said "select a column heading to sort", but only
the Organizations column sorts. The panel now gives just the bill count.
The Clients list keeps the hint, so its string stays.

Verified: on yarn dev, the Bills panel heading reads "5,660 bills with
reported lobbying" with no hint; check-types shows only the 4 known
errors; prettier passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Line the list pages' filter bar up with the list below it

The boxed bar holding the session select and the search box on the
Bills, Clients and Lobbyists pages was as wide as its filters, so it
ended short of the list's panel. A boxed bar now spans the list and its
search box takes the extra room (wrapping to its own line on phones).
The Overview's bar, with only the session select, keeps fitting its
filters (boxed="fit").

Verified: on yarn dev, the bar's and the list panel's edges match on all
three pages at 1280 and 375 px (82-1198 and 12-363), and the Overview's
bar still fits its select; check-types shows only the 4 known errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Restyle a lobbyist's page like the other lobbying pages

A lobbyist's page kept the older look: a compact Bootstrap table on the
gray background whose Period column was cut off, and a right column of
plain lists, 21c's clients alone running past 100 lines.

The Lobbied bills table now sits in a panel with the activity count and
its pagination, in the Lobbyists list's table style (a style of this
page's own; the bill card's shared table is unchanged). It scrolls
inside its panel on phones. Each right-column group is its own panel.
Clients collapse to their name and total, expanding to what each paid
by period; Registrations and Disclosures show the latest three, then
Show all / Show fewer.

NonBillActivity moves to its own file, and the shared filings table
drops the bill and amount columns its one caller, the bill card, never
showed. The ▸/▾ button style is shared with the activity tables.

Verified: on yarn dev, 21c at 1280 and 375 px (table fits its panel at
1280 and scrolls at 375; a client expands to its periods and collapses;
Registrations shows 3, all 12, then 3); Aaron Agulnek's panels; Aaron
Michael Vega's files-through-employer note inside the Bills panel;
yarn test:website passes; check-types shows only the 4 known errors.
Not verified: the bill card on MAPLE's bill pages in a browser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Run a filer's position history oldest to newest, captioned by season

An expanded row on the Bill and Client pages lists each filer's report
periods from left to right, oldest first, captioned "25-Spring" and
"25-Fall" (shown in capitals by the caption style) instead of "Jan–Jun 25".
The filer's SoS links follow the same order and name ("25-Spring SoS
filing"); a square's tooltip keeps the full "Jan–Jun 2025: Support".
sessionPeriods and sessionPeriodsWith now return oldest first, since this
list is their only user, and halfOfPeriod gives the year and half that the
caption is built from. The compact "Jan–Jun 26" label is gone, with
filingPeriodLabel's shortYear option.

Verified: jest components/lobbying (72 passed) and check-types (only the
4 earlier errors), on the tree with the next commit's layout too; on yarn
dev, H4746 and Abbott Laboratories show 25-SPRING, 25-FALL, 26-SPRING left
to right and links in that order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Give each period in a filer's history the same width

Every period takes at least the width of "26-SPRING" or two squares, so
Spring and Fall line up, and the "Most recent position" column is sized
from them to fit a session's four periods on one line even when a filer
reported two positions in a period; the filer side narrows to match. The
SoS links sit two a line, so a year's Spring and Fall filings pair up.

Verified: on yarn dev, Abbott Laboratories in the 193rd at 1280px: every
period 64px on one line, BCB Government Relations with Oppose and Neutral
in 23-Spring and 23-Fall; Maria Cahill's four links on two lines.
Not verified: a period with three positions, which widens its cell; at
phone width the table still scrolls sideways, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…elopment (so the Dev environment UX more closely matches prod). (#2350)
@vercel

vercel Bot commented Nov 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
maple-dev Ready Ready Preview Nov 6, 2026 5:13pm UTC
1 Skipped Deployment
Project Deployment Actions Updated
maple-prod Ignored Ignored Nov 6, 2026 5:13pm UTC

Request Review

@Mephistic
Mephistic merged commit e1c2907 into prod Nov 6, 2026
11 of 12 checks passed

This branch was successfully deployed

1 active deployment
Production – maple-dev — 3039f6dc Deployed Nov 6, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants