Skip to content

Add mass closure history page + API#107

Merged
retlehs merged 8 commits into
mainfrom
mass-closure-history
May 10, 2026
Merged

Add mass closure history page + API#107
retlehs merged 8 commits into
mainfrom
mass-closure-history

Conversation

@retlehs
Copy link
Copy Markdown
Member

@retlehs retlehs commented May 10, 2026

Closes #106

Summary

  • Adds a /closures history page that lists every recent vendor mass-closure event on WordPress.org, with per-vendor detail pages at /closures/{vendor-slug}.
  • An event is recorded whenever the same vendor has 2 or more plugins closed within a rolling 24-hour window. Detection piggybacks on the existing hourly status check — no new background job.
  • New JSON API (/api/closures, /api/closures/{vendor-slug}), RSS feed (/closures/feed), and .md variants for both pages. Replaces the old private-gist closure report.
  • Each vendor detail page shows every plugin in the outbreak with its current status (active / closed / tombstoned), pulled live from our package data so reactivations show through.
  • Status page expansions now render "tombstoned" actions correctly (previously silently dropped).

@retlehs retlehs self-assigned this May 10, 2026
retlehs and others added 7 commits May 9, 2026 20:02
- /api/closures returns page/per_page/total/total_pages alongside events
- /api/closures/{vendor} returns 404 + JSON error for unknown vendor
- Reserve "feed" slug so a vendor named "feed" can't shadow /closures/feed
- Add /closures/feed to nonNegotiableExact
- Decode HTML entities in author names before slugifying
- Add test for window-reset → new event branch
- Drop unnecessary AUTOINCREMENT on closure_events.id
- Use .VendorSlug instead of (index .Events 0).VendorSlug in vendor template

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- GetClosurePluginStatuses returns *ClosurePluginStatus so missing keys
  are nil; templates can distinguish unknown plugins (zero structs are
  truthy in Go templates, which silently mislabeled them as Closed)
- scanClosureEvent wraps detected_at parse errors with row id, matching
  the JSON unmarshal error handling
- API 404 for unknown vendor now includes documentation_url for parity
- RSS feed item descriptions list affected plugin slugs (up to 10 + "and
  N more"), so the feed is scannable in a reader

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add `id DESC` tie-breaker on `detected_at DESC` so events with
  identical timestamps render in a stable order
- RSS feed now serves `application/rss+xml` (matches the layout's
  `<link rel="alternate">` declaration)
- Wrap TrackMassClosures in a transaction so partial runs roll back
  cleanly; also tightens behavior under concurrent invocations

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lback

- RSS channel now declares an `<atom:link rel="self">` matching the
  declared `xmlns:atom`; item GUIDs marked `isPermaLink="false"` since
  they're fragment-style identifiers
- TrackMassClosures dedupes plugin slugs before checking the threshold
  so duplicate closure rows for one plugin can't create count=1 events
- Migration 030 down rollback now scopes by (vendor_slug, detected_at)
  so any real events recorded post-deploy aren't deleted on rollback
- Add slugify tests for the reserved-slug "feed" guard
- Check `rows.Err()` after iteration in the closure_events readers
- Drop unused Total from the closures handler data; comment the
  intentional non-fatal handling of GetClosurePluginStatuses errors

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- handleClosuresFeed now caches the rendered XML (1h TTL, mirrors
  handleFeed) and sets Cache-Control: public, max-age=3600
- TrackMassClosures, when creating a new event, subtracts slugs
  already recorded in this vendor's most recent event. The rolling
  24h candidate set can include source rows from a prior outbreak
  whose detected_at has aged out of cooldown but whose change rows
  are still in window — without this, slow-rolling vendors would get
  adjacent events sharing slugs. Regression test added.
- Drop unused closure.Time field (residual from when window_start
  was per-event); loadRecentClosures no longer SELECTs created_at
- Markdown vendor 404 returns a proper Markdown body instead of
  stdlib plain text, matching the JSON/HTML 404 surfaces

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- handleAPIClosures and handleAPIVendorClosures set
  Cache-Control: public, max-age=3600 to match the cron cadence
  (closures only change hourly), mirroring handleAPIClosedPackages
- Update the docs caching footnote (HTML + Markdown) to mention
  the closures endpoints
- loadRecentClosures sorts by scc.id so groupByVendor's first-seen
  display-name casing is deterministic if a vendor's plugins ever
  land with mixed author capitalizations

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- handleAPIVendorClosures only sets Cache-Control on success.
  Previously a 404 picked up max-age=3600 too, which could hide a
  vendor's first-ever mass closure behind a stale CDN-cached 404
  for up to an hour after the cron recorded it.
- renderClosuresRSS uses events[0].DetectedAt for lastBuildDate
  (with time.Now fallback when empty), so polling RSS readers stop
  seeing the channel "update" every hour during quiet periods.
  Mirrors the seo.go atom feed pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@retlehs retlehs merged commit 368a039 into main May 10, 2026
5 checks passed
@retlehs retlehs deleted the mass-closure-history branch May 10, 2026 02:12
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.

Mass-closure history page + API

1 participant