Where's Eric? Tracking NY politicians' public schedules

Note: I had mostly finished this project last weekend, before Eric Adams dropped out of the mayoral race. While Adams will still be the mayor until January, this project made more sense while he was an active candidate.

WHERE'S ERIC? provides a compilation and visualization of when Eric Adams and other New York officials have failed to make their public schedule available in advance, as reported by Politico.

As the New York City mayor's race escalates, I've been paying closer attention to the local politics-focused media outlets, including reading Politico's "New York Playbook" regularly. Aside from the actual news, they have a brief section where they ask: "Where's Kathy?" and "Where's Eric?", and summarize what their public schedules for the day are.

That is, if they receive them. Lately Adams' entry has been some variant of "Schedule unavailable as of 10 p.m. [previous night]".

I was curious what this actually meant in the long-term; was I coincidentally just reading Playbook on days he didn't provide his schedule? Or has he always been bad about providing his public schedule? Are other politicans any better?

Of course, the best way to answer this question was to look at literally the entire history of New York Playbook, so I processed the entire archive dating back to 2016 to get a more complete picture. The first Playbook issue that contained then-Governor Andrew Cuomo and then-Mayor Bill de Blasio's schedules was February 21, 2017: de Blasio had events in Manhattan and The Bronx while Cuomo had no public schedule.

Moving forward to 2025, I was mildly surprised to learn that Adams was actually perfect in providing his public schedule for the first three years of his term. Then on Friday, March 21, his first ever "Schedule unavailable as of 10 p.m. Thursday." appeared.

In April, he didn't provide his public schedule more often than he did. Over the past 8 weeks, his schedule has been unavailable 65% of the time.

This is...not great.

Knowing what our public officials are up to is a standard form of transparency that enables the press to document their actions so the public can hold them accountable. Not being up front with what you're doing undermines public trust, and while this might feel like a small thing, I think it's a decent indicator for how public officials respect the public and the press in general.

Given how chaotic the last few months of the Adams administration have been, part of me is curious whether this is due to incompetence or malice. We know quite well that Adams acts maliciously when it comes to the city hall press corps.

Where's Cuomo?#

And yet as bad as Adams is at this, he is still better than Andrew Cuomo, who, out of the four officials reported by Politico, is the worst.

WHERE'S ANDREW? shows how his record was consistently spotty since early 2017, but dramatically worsened in May 2020. Admittedly that was a pretty chaotic time for everyone, but this the same person who wanted us to celebrate his leadership during that time period.

Where's Bill and where's Kathy?#

During that same time periods Adams and Cuomo were failing at providing their public schedules, WHERE'S BILL? and WHERE'S KATHY? show in stark contrast that it was completely feasible to regularly provide their schedules.

Both provided their schedule to Politico 99% of the time, which I think shows that this is not a difficult task, and makes Adams' and Cuomo's failure to do so even more inadequate and unacceptable.

Methodology#

After scraping Politico's archive, the "Where's {name}?" fields were extracted into a database (raw data), with special handling for some edge cases. For example, on January 6, 2022, Politico had an joint item, "Where are Kathy and Eric?".

Also for about two weeks, Politico spelled it as "BlLL" (that's a lowercase L instead of an I). Oops.

A regular expression was used to identify days when the schedule was unavailable, specifically matching the phrases:

  • schedule unavailable
  • not available
  • schedule not available
  • schedule not released
  • unavailable as of
  • not released
  • by press time
  • schedule yet
  • no public schedule released as of
  • no public schedule available as of
  • as of {number}

Notably this does not match when a schedule was provided but there were no public events.

I performed a spot check against most of the unavailable dates and far fewer of the available ones, erring on the side of identifying false positives. If you do find an error, please contact me.

Major credit and thanks to the Politico reporters for collecting and reporting this data for nearly a decade.


Adventures of a YAML engineer

I want to brag about a bit of YAML code I wrote back in March for SecureDrop's completed migration to Ubuntu Noble that I neglected to mention in the blog post explaining the technical details. Yes, YAML, is a programming language.

We offered SecureDrop Administrators the option for a "semiautomated" upgrade: they run one command, ./securedrop-admin noble_migration, and it'll take care of the rest. The main advantage for doing so was that the upgrade would happen at the time you chose, and if something happened to go wrong, you were already on hand to deal with it!

Under the hood the semiautomated upgrade was starting an Ansible playbook that edited our JSON control file to mark the server as ready to be upgraded and then started the systemd service. And then it just waits until the upgrade completes, which ended up being the harder part to implement.

During the upgrade, the server reboots twice (once before installing updates and once after), which means Ansible will lose its SSH connection. I used Ansible's wait_for_connection module to reconnect instead of error out, and naively had it wait for that to happen twice before checking if the upgrade had finished.

But during testing we found a problem when using SSH-over-Tor, in which Ansible would disconnect three times. It disconnected on the first pre-upgrade reboot, then during the upgrade when the Tor package was restarted, and then again during the second post-upgrade reboot.

And, to make it even more fun, this was subject to a race condition. In at least one instance, it took long enough for Tor to come back that the server rebooted before it reconnected, so there were only two disconnections.

Knowing that, a naive solution wasn't going to cut it anymore, so I implemented the same state machine as the Rust code, just in the YAML playbook. It now parsed the JSON state file, looked up where in the overall process it was, and then calculated how many reboots are likely remaining. Once it disconnected and reconnected, it looked at the state file again, so it knew how many more to expect.

Here's the end result, it ended up being just over 200 lines of YAML (including comments).

Alternative clickbait titles for this post include: "Porting some of my Rust code to YAML" and "Writing a state machine in YAML".


A small change in plans

A small change in plans: I'm starting law school in the fall. I'll be attending the CUNY School of Law right here in Queens to become a public interest-focused lawyer.

I plan to continue working full time at the Freedom of the Press Foundation and go to school in the evenings, part time. And yes, law school is something I have always wanted to attend.

Going forwards you'll probably see me switch up the standard disclaimer to something like IANALY (I Am Not A Lawyer Yet).


In support of Zohran Mamdani

Unfortunately I don't have 8.3 million dollars to spend in support of a political candidate, but I do have my blog.

In the ongoing New York City mayor's race (specifically the Democratic primary), I'm supporting, canvassing and voting for my assembly member, Zohran Mamdani. His entire platform is centered around making NYC more affordable, specifically:

  1. freezing the rent for rent-stabilized tenants (previously done by de Blasio)
  2. making buses fast and free (he won a 1-year pilot on this, it was reasonably successful)
  3. free childcare (I didn't have a parenthetical for this)

This is not to mention his various plans to build more housing, both creating new public housing and speeding up construction of private housing.

And he has a plan to create a "Department of Community Safety", which will task dedicated professionals and mental health experts on helping people with homelessness and other crisis response. And that will let police do actual police things.

I know for sure that he can deliver on the first part of his platform, freezing the rent, since the mayor appoints all the members of the rent control board. The rest requires collaboration from the city council and most likely Albany.

It's not a guarantee that it's possible, but if he wins, there will be a public mandate for it, and suddenly, it'll be realistic.

Ultimately I want a mayor who is willing to try new ideas instead of constantly being stuck doing what is "safe" and continuing old policies that have gotten us here. Zohran is that person and my #1 vote.

Brad Lander#

The more I learn about Brad Lander, the more I like him. Out of all the candidates (including Zohran), I think he is best suited to hitting the ground running as mayor on day one. He seems to have the best grasp on the NYC bureaucracy and has incredibly detailed and technical plans on how to address, well, everything.

I ranked him #2 (in line with Zohran's cross endorsement), but respect and support anyone who ranks him #1 and Zohran #2. If Zohran ends up winning, I hope he gives Brad Lander a significant role in his administration.

Don't Rank Evil Andrew for Mayor#

I never actually lived in New York during Andrew Cuomo's tenure, but I've read enough from the time and everything that's come out since. The fact that he was governor for 10 years, and HUD secretary for another 4 means that he had the opportunity to fix it in the past, but didn't. It's time for new leadership.

I think this is a perfect example showing that letting people voluntarily resign under pressure is a bad idea; if he had been impeached and removed from office, there wouldn't have been a comeback.

Final thoughts#

Zohran has been a great representative for me, and I am looking forward to sharing him with the rest of the city.

I told someone once that I'm supporting Zohran because as my assembly member, he's the first elected official to represent me that I'm not embarrassed by. I don't mean that we agree on everything (we mostly do, but not 100%) — rather I think he has a good set of core guiding principles, and sticks by them in ways that are understandable and justifiable.

After having an incredibly embarrassing mayor for the past 4 years, I'm looking forward to one I respect and appreciate. I hope you'll rank Zohran #1 (and Lander #2).

Voting is open today, June 22 (9am-5pm), and then again for the last time on June 24 (6am-9pm).


Creating IPv4-only and IPv6-only containers with podman

By default, newer versions of podman run containers with a dual stack network that supports IPv4 and IPv6 (yay). But if you're doing something specific, you can set up IPv4-only and IPv6-only networks.

(Note: I tested this all with rootless podman 5.5.0, the current version in Fedora 42.)

I'm primarily writing this because it took me a while to figure this out, I got entirely tripped up by the --ipv6 option which turned out to not be what I wanted, despite the name implying it enables IPv6.

The documentation for it is technically accurate, as it says:

Enable IPv6 (Dual Stack) networking. If no subnets are given, it allocates an ipv4 and an ipv6 subnet.

The most important part is in parenthesis — it enables a dual-stack network. Which means that passing --ipv6 when creating a network doesn't just enable IPv6, it also enables IPv4!

Real IPv6-only#

What you actually want is:

$ podman network create --subnet fd00::/64 --gateway fd00::1 ipv6-only

You can verify that IPv4 doesn't work by:

$ podman pull quay.io/curl/curl:latest
$ podman run --rm -it --net=ipv6-only curl -v4 https://en.wikipedia.org
* Host en.wikipedia.org:443 was resolved.
* IPv6: (none)
* IPv4: 208.80.154.224
*   Trying 208.80.154.224:443...
* Immediate connect fail for 208.80.154.224: Network unreachable
* Failed to connect to en.wikipedia.org port 443 after 13 ms: Could not connect to server
* closing connection #0
curl: (7) Failed to connect to en.wikipedia.org port 443 after 13 ms: Could not connect to server

And that IPv6 works:

$ podman run --rm -it --net=ipv6-only curl -I6 https://en.wikipedia.org
HTTP/2 301 
date: Fri, 23 May 2025 00:29:41 GMT
...

IPv4-only#

And now for IPv4, which is even simpler:

$ podman network create ipv4-only

Yep, no options needed, you just need a network in which IPv6 is not enabled by the subnet and doesn't pass the --ipv6 flag.

Final notes#

The default networking stack for rootless containers is documented (under "pasta") as "IPv4 and IPv6 addresses and routes, as well as the pod interface name, are copied from the host". In my testing this is correct, but this is an entirely separate thing from podman network that appears to exist by default, which is IPv4-only.

I ended up figuring out the whole misleading --ipv6 thing thanks to a GitHub comment, which explicitly spelled out "The --ipv6 flags means dual-stack", and even explained the rationale why: "this is fully compatible with docker ..."

I shouldn't be too surprised that Claude also got tripped up by the --ipv6 flag and gave me bad advice. ¯\_(ツ)_/¯

Final final note: if you try a plain podman run curl ... without first pulling the image, it won't know which image you actually want, and none of the three prompts it gives you (registry.fedoraproject.org, registry.access.redhat.com, docker.io/library) are the official upstream image. I've submitted a PR to the containers/shortnames repo to fix that, so a plain curl image name will automatically be aliased to the upstream image.


Archiving Hell Gate's FYIs

Hell Gate is my favorite New York City-focused news outlet. The coverage is good and the writing tends to be exceptional.

When they redesigned their website in July last year, they added a new "FYI" section, which is usually one to two sentences with a link to a story about some current event. As I write this, the current FYI is:

New pope. Way too quick. Suspicious.

Maybe not the best example. Before that, it was:

In Randy Mastro's New York City, there will be no Pride concerts at Central Park if you support Palestine.

They usually update it every 3-4 days, though during major news events it might be more frequent.

Unfortunately it's not well advertised; on mobile, it's buried in a menu that you need to open before you even learn it exists. Plus they don't always post them on their social media and it's not in their newsletters.

So I've taken it upon myself to create an archive of them and provide an RSS feed. If you visit https://legoktm.com/hellgatenyc-fyi/, you'll see a (hopefully) complete archive of all their FYIs, using a layout and theme that tries to look like Hell Gate's website. It was a fun quick trip through the past year of NYC.

I also added a people filter, so you can just see entries that mention Mayor Eric Adams, disgraced former Governor Andrew Cuomo, and current governor Kathy Hochul. In a surprising-but-not-really-that-surprising twist, the former governor, who is also the leading mayoral candidate, has more entries than the current one.

How I built it#

I started by scraping the Wayback Machine for all the old entries.

Getting the "FYI" out of the HTML was trivial, the script looked for the node that matched the CSS selector .fyi-section p. If the inner HTML was different than what was previously found, it was saved as a new entry. (As a weird contradiction, Hell Gate's website adds both ?ref=hellgatenyc.com to any URL, and sets rel="noreferrer" 🙃.)

The Wayback Machine has some pretty aggressive rate limits, which was annoying for a bit, until I realized I could plug in urllib3's Retry utility and have it, slowly, retry everything until it succeeded. Some days the Wayback Machine had archived Hell Gate's homepage like every 10 minutes so I ended up adding an optimization to skip entries that were within 3 hours of one I already checked (hopefully it didn't miss anything).

Now that I had collected ~75 entries in a JSON database, I wrote a small Rust program to identify new entries and export a RSS feed on a 3-hour timer.

When I started manually reviewing all the entries, I realized that some of them were just typos or other cosmetic changes. For example, back in August 2024:

- To truly understand Bryant Park, y<a href="https://hellgatenyc.com/bryant-park-frog-carousel-flaubert-mystery/">ou must wrestle with its large frog</a>
+ To truly understand Bryant Park, <a href="https://hellgatenyc.com/bryant-park-frog-carousel-flaubert-mystery/">you must wrestle with its large frog</a>

The "y" didn't get linked, and within a few hours they fixed it.

I applied two checks to detect these type of typo entries. First, seeing if the plain text version is the same, to detect links changing or issues like the one above. Then I added in a check for the Levenshtein distance to detect other cases of minor changes.

Even with those two checks, it's not perfect. Sometimes the edits are more substantial, like "George Santos has been sentenced to more than seven years...". The additional "more than" is more than a small typo fix, but still just a correction.

But then there are FYIs like "What are you doing on December 5TONIGHT? ..." Only two words being changed, but it feels like both merit independent entries. The ideal solution would be manual curation, but I don't think I can commit to that, so the current implementation is a reasonable compromise for now.

The last part of this project was creating a HTML browser for all of these, which would allow linking to old FYIs. I tried pretty hard to mimic the styling of the Hell Gate website, which was fun.

It's weird what you learn when you dig very deeply into a website's CSS. On the Hell Gate website, if you hover over an author link, after 2 seconds it turns purple. I never noticed!

Nearly everything draws from elements on the Hell Gate website, except I wasn't able to replicate the font used in the headlines because it's not freely licensed. They use Futura Passata; I looked for free equivalents to Futura and ended up with "League Spartan", which is not really close, but in the ballpark at least. The body text is correctly "Outfit".

I'm exceptionally pleased with how the people filter turned out. In the database, I wrote some code to tag entries based on who was mentioned. "Adams" maps to Eric Adams, unless it's Adrienne Adams (no relation); "Cuomo" maps to Andrew Cuomo unless it's Chris Cuomo (yes relation).

This was especially fun to implement since Rust's primary regex crate doesn't support negative lookaheads.

On the HTML side, it's a radio input element, so only one filter can be selected at a time. The actual filtering is implemented in pure CSS:

body:has(#filter-adams:checked) .entry:not(.person-Adams) {
    display: none;
}
body:has(#filter-cuomo:checked) .entry:not(.person-Cuomo) {
    display: none;
}
body:has(#filter-hochul:checked) .entry:not(.person-Hochul) {
    display: none;
}

The only issue I ran into is that Firefox helpfully remembers the radio button state you last used, which isn't what I wanted here. I ended up adding a few lines of JavaScript to take care of it for now:

document.addEventListener("DOMContentLoaded", () => {
    document.querySelectorAll('input[type="radio"]').forEach((elem) => {
        elem.checked = false;
    });
});

That's pretty much it, I've published the source code for those that want to peek.