mirror of
https://github.com/misenhower/splatoon2.ink.git
synced 2026-09-29 20:56:35 -05:00
314 lines
17 KiB
Markdown
314 lines
17 KiB
Markdown
# Splatoon 2 updater Worker
|
|
|
|
Runs the existing updaters and Bluesky posts on Cloudflare. `BucketStorage` uses
|
|
R2 instead of the local runner's `dist/` and `storage/` directories. Private
|
|
state stays in R2; no database migration is needed.
|
|
|
|
## Scheduling and manual runs
|
|
|
|
One `Scheduler` Durable Object owns the update → social pipeline. Its alarm
|
|
targets ten seconds past each hour, matching the old Node cron. Alarms normally
|
|
have low drift, but maintenance/failover can delay them; this is not a hard
|
|
real-time deadline. The `wnam` location hint is best effort and correctness does
|
|
not depend on where the object runs. There is no per-run colo probe.
|
|
|
|
- The pipeline runs all updaters and posts only if all succeed.
|
|
- A failed hourly pipeline retries after one minute, up to three retries, then
|
|
returns to the next hour. Successful posts keep their existing per-post
|
|
Bluesky timestamps so a retry skips them.
|
|
- The minute-30 cron checks/re-arms the alarm. The object may sleep between
|
|
alarms; the watchdog repairs scheduling, rather than keeping a process alive.
|
|
- Manual runs use the same object. A request during an active run returns HTTP
|
|
409; retry it later. The admin panel can persist one background manual request.
|
|
There is no `/wake` or `/post` endpoint. An hourly alarm that encounters a
|
|
manual run remains due.
|
|
- `POST /run` runs the complete pipeline and waits for its result. With `only`,
|
|
it repairs the named updaters and skips social posting. Unknown names fail.
|
|
- The existing object name and hourly/retry state survive deployment. Pending
|
|
requests from the old scheduler become one full run before normal scheduling
|
|
resumes.
|
|
|
|
`GET /status` reports the due time, retries, last hourly run, last manual run,
|
|
and whether the object is busy. Run summaries include actual start/completion
|
|
information, updater results and per-post/client social outcomes. HTTP runs
|
|
return 200 for success, 409 when busy, or 502 for a failed pipeline. Storage/RPC
|
|
errors return 500. Inspect the response body and Worker logs for details.
|
|
|
|
Only one backend may write production at a time. Coordination in this Worker
|
|
does not serialize it with the old server or a locally started runner. A social
|
|
API accepting a post followed by a failed timestamp write still leaves an
|
|
ambiguous outcome; storage cannot make those two external operations atomic.
|
|
|
|
## Social posts and screenshots
|
|
|
|
### Public routing and caching
|
|
|
|
Cloudflare Pages serves the frontend. Cloud Connector serves individual
|
|
`/data/`, `/assets/splatnet/`, and `/twitter-images/` files directly from R2
|
|
at their original site URLs; do not redirect file requests to the asset host.
|
|
Production `/data`, slash-terminated `/data/` directories, and slash-terminated
|
|
`/assets/splatnet/` directories redirect to the browser at `assets.splatoon2.ink`.
|
|
The asset-browser Worker handles directory listings only.
|
|
|
|
Cloudflare's `Long-lived immutable media` cache rule uses a one-year edge TTL
|
|
for production `/assets/` files (excluding directories and HTML), and image
|
|
files under `assets.splatoon2.ink/assets/splatnet/`. The zone's Browser Cache TTL
|
|
is **Respect Existing Headers**; the old `/assets/*` Page Rule that forced a
|
|
one-month browser TTL is disabled. These settings are managed in Cloudflare,
|
|
not Wrangler. Do not add a browser TTL override to obtain longer edge caching.
|
|
|
|
Rotating public images under `/twitter-images/` reuse their filenames. The
|
|
updater writes them with `Cache-Control: no-cache` so caches revalidate before
|
|
reuse. Keep them outside the long-lived asset rule. On September 15, 2026,
|
|
production routing and cache settings were verified, the existing schedule and
|
|
gear PNGs received this metadata, and their old edge-cache entries were purged.
|
|
|
|
### Posting pipeline
|
|
|
|
`src/app/social` posts to Bluesky. Twitter/X support and its dependency have
|
|
been removed. Local commands are `npm run social` and `npm run social:test`.
|
|
|
|
The existing `bluesky-lastPostTimes.json`, `salmonrun-previousSchedule.json`,
|
|
and `stages.json` keys are preserved. Public images remain at `twitter-images/`
|
|
to preserve existing URLs; that legacy directory name does not enable X posting.
|
|
With no Bluesky credentials the runner generates public images only. Shadow
|
|
runs still update some private state, including the remembered Salmon Run shift.
|
|
|
|
Each post captures one PNG. The public copy stays in R2; when Bluesky needs
|
|
JPEG, the Worker's `IMAGES` binding converts those same PNG bytes at quality 90.
|
|
The local Node runner uses `sharp` through a conditional package import, keeping
|
|
native dependencies out of the Worker bundle. Conversion failures fail that
|
|
client's post without advancing its timestamp; no second browser capture is used.
|
|
Public-image-only shadow runs do not request conversions.
|
|
|
|
The Images Free plan includes 5,000 unique transformations per month; on that
|
|
plan, new transformations beyond the quota fail instead of incurring charges.
|
|
This uses transformations only, not paid Images storage. See
|
|
[Images pricing](https://developers.cloudflare.com/images/pricing/).
|
|
|
|
Browser Rendering opens `SITE_URL/screenshots.html`. Before rendering or
|
|
posting, the Worker compares that site's five data JSON files and English
|
|
localization against its own public bucket. A mismatch fails the run so the
|
|
scheduler can retry. Point `SITE_URL` at a preview site serving the dev bucket
|
|
to validate shadow output; production is suitable only when its data matches.
|
|
The screenshot request asks the browser to revalidate cached resources. The
|
|
preflight validates JSON, not pixel output or an atomic snapshot across a CDN.
|
|
Visually check generated images before cutover.
|
|
|
|
Nintendo, Bluesky, and rendering-site checks have 30-second network deadlines,
|
|
including body consumption. Browser Rendering uses 10-second navigation, page-ready, and capture limits,
|
|
through the Browser binding. It waits for
|
|
`data-screenshot-ready="true"` after data, Vue rendering, fonts, images and layout
|
|
settle, instead of waiting for network idle. Deploy the updated screenshot page
|
|
before the Worker that requires this marker.
|
|
|
|
Transient screenshot errors (timeouts, network failures, and HTTP 5xx) get up to
|
|
three retries after the first attempt, with 0.5/1/2-second backoff. Authentication,
|
|
rate-limit and non-timeout validation errors fail immediately. These retries only
|
|
repeat rendering, never the social send; exhausted failures still reach the hourly
|
|
pipeline's bounded retry mechanism. Errors are reported rather than logged
|
|
as successful social runs.
|
|
|
|
## Local screenshots and Browser Run testing
|
|
|
|
Node commands use Puppeteer; the Worker uses `BrowserRunRenderer` with its
|
|
`BROWSER` binding. `ScreenshotGenerator` owns the shared routes, viewport, and
|
|
page-ready selector; either renderer returns PNG bytes. No API token or account
|
|
ID is needed for screenshots, and no package import conditions select renderers.
|
|
|
|
With Puppeteer, leave `SITE_URL` empty to temporarily serve the built `dist/`
|
|
on loopback. Run `npm run build` first and provide the usual data/assets in
|
|
`dist/`. Puppeteer installs its own Chrome; Browserless is not required.
|
|
Set `SITE_URL=http://127.0.0.1:8080` to use `npm run serve` instead. The Vue dev
|
|
server already serves data/assets from `dist/`; keep those files aligned with
|
|
the data the social test reads. Local rendering waits for the page-ready signal
|
|
and uses 10-second navigation, readiness, and browser-protocol timeouts.
|
|
|
|
```sh
|
|
# Generate the social test images against the local build.
|
|
SITE_URL= npm run social:test
|
|
|
|
# Capture one route; replace the timestamp with a rotation in your data.
|
|
SITE_URL= npm run screenshot -- \
|
|
--hash '/schedules/1788652800' --output dist/test-screenshots/schedule.png
|
|
|
|
# Capture a running dev server directly.
|
|
npm run screenshot -- \
|
|
--url 'http://127.0.0.1:8080/screenshots.html#/schedules/1788652800'
|
|
|
|
# Start the capture-only Worker locally, with a remote Browser binding.
|
|
npm run screenshot:cloudflare
|
|
|
|
# In another terminal, save a capture from a publicly reachable screenshot page.
|
|
curl --fail-with-body --get 'http://127.0.0.1:8789/' \
|
|
--data-urlencode 'url=https://dev.splatoon2.ink/screenshots.html#/schedules/1788652800' \
|
|
--output /tmp/cloudflare.png
|
|
```
|
|
|
|
`npm run screenshot` loads `.env`, accepts either `--url` or `--hash`, and saves
|
|
a PNG using local Chrome. `--hash` uses `SITE_URL` or the temporary dist server.
|
|
|
|
`npm run screenshot:cloudflare` runs the small `workers/screenshots` entry point
|
|
through Wrangler. Sign in with `npx wrangler login` if needed. Only the browser
|
|
runs remotely; the capture endpoint listens on loopback port 8789. This entry
|
|
point is for local development, not deployment. Cloudflare cannot reach localhost:
|
|
use a deployed preview or tunnel for an unpublished frontend. Remote captures
|
|
use the account's Browser Run allowance.
|
|
|
|
Both paths are capture-only: no updates, social sends, checkpoints, dataset
|
|
comparisons, or JPEG conversion. All screenshot pages must provide the readiness
|
|
marker. The updater's social pipeline retains its published-data checks.
|
|
|
|
## Shadow testing and cutover
|
|
|
|
The `dev` environment deploys `splatoon2-ink-dev-updater`, using
|
|
`splatoon2-ink-dev-assets`, `splatoon2-ink-dev-private`, and
|
|
`https://dev.splatoon2.ink`. The `production` environment deploys
|
|
`splatoon2-ink-updater`, using the production buckets and site. Each Worker owns
|
|
its own scheduler and secrets. Leave Bluesky credentials unset in dev.
|
|
|
|
The npm updater development, deployment, dry-run and tail commands select dev.
|
|
Use `npm run updater:deploy:production` for a manual production deployment.
|
|
Neither environment starts automatic scheduling on a fresh scheduler.
|
|
|
|
Cloudflare Workers Builds deploys pushes to `develop` to the dev Worker and
|
|
pushes to `main` to the production Worker. Both use these dashboard settings:
|
|
|
|
- Repository root: `/`
|
|
- Build command: `npm run lint -- --max-warnings 0 && npm test`
|
|
- Deploy command: `npx wrangler deploy --config workers/updater/wrangler.jsonc --env dev`
|
|
for dev, or the same command with `--env production` for production
|
|
- Build variable: `NODE_VERSION=22`
|
|
- Preview builds for other branches: disabled
|
|
|
|
The frontend deploys separately through Cloudflare Pages. Deployments preserve
|
|
the stored scheduling toggle; they do not enable automatic updates.
|
|
|
|
Production cut over on September 13, 2026. The old backend is stopped, public
|
|
data and private checkpoints are in the production R2 buckets, and hourly
|
|
scheduling is enabled. The production admin panel is at
|
|
`https://admin.splatoon2.ink/`, protected by its own Access application using
|
|
the same owner-only policy as dev.
|
|
|
|
1. Run the tests, build, and deployment dry run below. Compare old/new public
|
|
data using `scripts/compare-data.mjs` on downloaded bucket directories.
|
|
2. Serve the built frontend with `/data/` and `/assets/` backed by the dev
|
|
bucket, then set `SITE_URL` to that preview site's origin. Keep Bluesky
|
|
credentials unset. Run the full pipeline and visually inspect its images.
|
|
3. At cutover, stop the old scheduler and let its current run finish. Pause the
|
|
shadow Worker before copying state: clearing the cron alone does **not**
|
|
stop the object's existing alarms. Do not leave two writers active.
|
|
4. Copy the latest production private state (especially the Bluesky last-post
|
|
times and previous Salmon Run shift) to the intended Worker private bucket.
|
|
Preserve an old-state backup and the old deployment for rollback.
|
|
5. Set `ASSETS` to the production bucket, confirm `PRIVATE`, and set `SITE_URL`
|
|
to the production site that serves that bucket. Add the Bluesky credentials.
|
|
Deploy and arm the Worker; check its full run, data freshness, images, and
|
|
next alarm. Deploy the frontend refresh changes as part of this rollout.
|
|
6. For rollback, stop the Worker/alarms before restarting the old backend.
|
|
Transfer the latest Bluesky checkpoints back so already-sent posts remain
|
|
recorded. Restore the prior site/data configuration as needed.
|
|
|
|
These are rollout steps, not actions performed by tests or a dry run. To stop
|
|
this Worker safely for cutover/rollback, use authenticated `POST /pause`; this
|
|
persists the pause and deletes the alarm. `/arm` resumes scheduling; a saved
|
|
overdue run executes immediately. Do not pause
|
|
in the middle of a run: a busy pause request returns 409 so the caller can retry.
|
|
|
|
## Admin panel
|
|
|
|
The admin panels at `https://admin.splatoon2.ink/` and
|
|
`https://admin.dev.splatoon2.ink/` provide data-only, social-only, and full
|
|
manual runs. The authenticated browser starts a persisted request and polls its
|
|
status; closing the tab does not cancel the job. One manual request can be
|
|
pending at a time, and it shares the hourly scheduler's lock. Social runs keep
|
|
normal checkpoints and the published-data check. An interrupted manual run is
|
|
reported as failed rather than automatically replaying an uncertain social send.
|
|
The hourly schedule is preserved. Paused scheduling also blocks manual runs.
|
|
|
|
The panel polls live application log lines every two seconds during a run and
|
|
retains the latest 50 manual and scheduled runs in one history, including failed
|
|
attempts. Older deployments contribute their two existing results. Each run
|
|
stores up to the latest 200 lines (500 characters each, 32 KB total) with its
|
|
final summary.
|
|
Known secret values are redacted from captured lines. This includes updater,
|
|
social, and screenshot-retry messages, not platform or third-party library logs.
|
|
Live lines are held in memory until completion; an isolate interruption can lose
|
|
those lines. Full operational logs remain available through Workers logging.
|
|
|
|
Preview the actual panel with simulated results using `npm run admin:preview`,
|
|
then open `http://127.0.0.1:8788/admin/`. This standalone preview server listens
|
|
only on loopback and has no production credentials or bindings. The production
|
|
Worker has no local-authentication bypass.
|
|
|
|
The dev admin hostname is attached as a Worker Custom Domain and protected by
|
|
the "Splatoon2 dev admin" Access application using the existing owner-only policy.
|
|
`ACCESS_TEAM_DOMAIN` and `ACCESS_AUD` are stored as Worker secrets to keep
|
|
account-specific configuration out of the public repository. `/admin/` remains
|
|
an alias; the panel API stays under `/admin/api/`.
|
|
|
|
For another deployment:
|
|
|
|
1. Create a Cloudflare Access self-hosted application protecting the entire
|
|
chosen admin hostname. Allow only the
|
|
owner's identity, with email one-time codes or their preferred provider.
|
|
2. Configure the updater with `ADMIN_HOSTNAME`, `ACCESS_TEAM_DOMAIN` (the bare
|
|
`<team>.cloudflareaccess.com` hostname), and `ACCESS_AUD` (the application's
|
|
audience tag). All admin routes fail closed without these settings.
|
|
3. Attach the admin hostname to this Worker and deploy. Open `/` and verify
|
|
login, status, and a deliberate test run. No Access application or production
|
|
admin domain is created by the local preview.
|
|
|
|
The Worker verifies JWT signature, issuer, audience, expiration, and hostname.
|
|
Mutating browser requests also require a matching Origin and JSON content type.
|
|
The existing bearer-token API remains available for scripts, independently of
|
|
Access. The panel exposes an **Automatic scheduling** toggle. Its value is saved in the
|
|
scheduler Durable Object and survives deployments. Turning it off clears the
|
|
hourly/retry schedule, but manual runs still work. The watchdog respects the
|
|
setting, so leave its cron configured. Turning it on schedules the next hour
|
|
at :00:10; missed hours are not replayed. Wait for an active or queued run to
|
|
finish before toggling.
|
|
|
|
The separate API-only `/pause` remains a maintenance stop: it also blocks manual
|
|
runs. `/arm` clears that maintenance pause without changing the automatic setting.
|
|
The panel does not expose force-repost or maintenance pause/resume controls.
|
|
|
|
## Configuration and local commands
|
|
|
|
Secrets: `NINTENDO_SESSION_ID_NA`, `NINTENDO_SESSION_ID_EU`,
|
|
`NINTENDO_SESSION_ID_JP`, optional `SPLATNET_USER_AGENT`, `RUN_TOKEN`,
|
|
optional `SENTRY_DSN`, and at cutover
|
|
`BLUESKY_SERVICE`, `BLUESKY_IDENTIFIER`, `BLUESKY_PASSWORD`.
|
|
|
|
Use `wrangler secret put NAME --config workers/updater/wrangler.jsonc --env dev` for a
|
|
secret. `SITE_URL` is a non-secret var in the config. Screenshots use the
|
|
`BROWSER` binding; no Browser Run API credentials are required.
|
|
For local development, use gitignored `workers/updater/.dev.vars.dev`. The existing
|
|
shared code reads these values through Workers' populated `process.env`.
|
|
Sentry wrappers route shared updater errors to Sentry when `SENTRY_DSN` is set.
|
|
|
|
```sh
|
|
npm test
|
|
npm run lint
|
|
npm run build
|
|
npm run updater:deploy:dry-run
|
|
npm run updater:dev
|
|
npm run updater:tail
|
|
```
|
|
|
|
Operator requests require `Authorization: Bearer $UPDATER_RUN_TOKEN`, matching
|
|
the deployed `RUN_TOKEN` secret. Without `RUN_TOKEN` the endpoints are disabled.
|
|
|
|
```sh
|
|
BASE=https://splatoon2-ink-updater.<subdomain>.workers.dev
|
|
curl -X POST -H "Authorization: Bearer $UPDATER_RUN_TOKEN" "$BASE/run"
|
|
curl -X POST -H "Authorization: Bearer $UPDATER_RUN_TOKEN" "$BASE/run?only=Schedules,Timeline"
|
|
curl -X POST -H "Authorization: Bearer $UPDATER_RUN_TOKEN" "$BASE/arm"
|
|
curl -X POST -H "Authorization: Bearer $UPDATER_RUN_TOKEN" "$BASE/pause"
|
|
curl -H "Authorization: Bearer $UPDATER_RUN_TOKEN" "$BASE/status"
|
|
curl -H "Authorization: Bearer $UPDATER_RUN_TOKEN" "$BASE/list?prefix=data/"
|
|
```
|
|
|
|
`/list` returns at most 1000 public-bucket keys and reports truncation. It is a
|
|
small diagnostic endpoint, not a full bucket export tool.
|