Commit Graph

481 Commits

Author SHA1 Message Date
Matt Isenhower
09caa34d7d Clarify Node runner names 2026-09-07 17:12:28 -07:00
Matt Isenhower
74cd311151 Serve admin panel at protected dev custom domain 2026-09-07 14:05:40 -07:00
Matt Isenhower
bfc35b9f99 Make admin panel source easier to read 2026-09-07 13:48:08 -07:00
Matt Isenhower
6c44b548b6 Simplify run helpers and clarify scheduler control flow 2026-09-07 13:48:06 -07:00
Matt Isenhower
b8753da78d Preserve original Vue component formatting 2026-09-07 13:48:03 -07:00
Matt Isenhower
d7581e3e8c Remove obsolete JPEG browser screenshot options 2026-09-07 13:46:05 -07:00
Matt Isenhower
41ac871fda Remove decorative introduction from the admin panel 2026-09-07 13:42:40 -07:00
Matt Isenhower
f9154a1dfb Show live and retained run logs in the admin panel 2026-09-07 13:42:25 -07:00
Matt Isenhower
ec797c4e13 Add an Access-protected admin panel and local interactive preview 2026-09-07 13:37:19 -07:00
Matt Isenhower
01d2b87335 Persist manual data and social runs for background execution 2026-09-07 13:37:19 -07:00
Matt Isenhower
8aa90337fd Keep environment variants and account configuration private 2026-09-07 13:22:14 -07:00
Matt Isenhower
a87855644d Convert social screenshots to JPEG without a second browser capture 2026-09-07 13:19:31 -07:00
Matt Isenhower
50156e96e1 Retry transient Browser Run screenshot failures with bounded backoff 2026-09-07 13:09:31 -07:00
Matt Isenhower
9c38064843 Wait for explicit screenshot readiness with shorter browser phase limits 2026-09-07 13:09:15 -07:00
Matt Isenhower
a20e211c1b Refresh site data when a stale browser tab becomes visible 2026-09-07 13:04:38 -07:00
Matt Isenhower
bc377f3cd6 Document social operation, shadow validation, and safe Worker cutover 2026-09-07 13:01:04 -07:00
Matt Isenhower
1f2b19c7f1 Run backend tests and frontend build in CI 2026-09-07 13:01:02 -07:00
Matt Isenhower
f61f286f82 Retry browser data refreshes after delayed publication or failed requests 2026-09-07 13:00:41 -07:00
Matt Isenhower
31e8f26db1 Check rendering site data against the published bucket before posting 2026-09-07 13:00:38 -07:00
Matt Isenhower
1b430d172a Simplify hourly scheduling and serialize all updater runs 2026-09-07 13:00:14 -07:00
Matt Isenhower
a0badb5f39 Bound upstream and screenshot requests with network deadlines 2026-09-07 12:58:15 -07:00
Matt Isenhower
8a38942e3d Report social posting failures and retain successful post checkpoints 2026-09-07 12:57:27 -07:00
Matt Isenhower
235455bfea Remove X posting and rename the bot commands and modules to social 2026-09-07 12:56:53 -07:00
Matt Isenhower
9258447df7 Run the social posters in the Worker after the updaters
The Scheduler's hourly alarm now runs two jobs in order, updaters then
posters, matching the container; a failure in either schedules the
retry. POST /post runs the posters by hand and wake?job=posters through
the alarm. The posters read process.env, which Workers populate from the
bindings: SITE_URL and CLOUDFLARE_ACCOUNT_ID are vars, the Browser
Rendering token and the social credentials are secrets. Without social
credentials the posters only render and save the public images, which is
the shadow mode until cutover.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 19:06:48 -07:00
Matt Isenhower
55c1d3ae81 Render screenshots with Cloudflare Browser Rendering
The screenshot module posts the deployed screenshot page's URL and the
viewport to Browser Rendering's REST endpoint and gets the image back, so
the same code runs under Node and in a Worker. It waits for network idle
as the puppeteer version did. Puppeteer, the local HTTP server, and the
browserless configuration are gone.

Posts render once per media type a client needs: PNG for the public copy
and Twitter, JPEG for Bluesky, which limits image size. That replaces the
sharp conversion in the Bluesky client, which now takes the media as
given and includes the aspect ratio when the size is known (from the
viewport for screenshots, from the PNG header for stage images).

Configuration mirrors splatoon3.ink: SITE_URL, CLOUDFLARE_ACCOUNT_ID, and
CLOUDFLARE_BROWSER_RUN_API_TOKEN.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 19:04:52 -07:00
Matt Isenhower
4a408a04c5 Move the posters onto public and private storage
The post generators read site data through publicStorage and their state
(last post times per client, the previous Salmon Run shift, known stages)
through privateStorage, the same two storages the updaters use, so they
run unchanged under Node and in a Worker. The public copy of each image
goes to twitter-images/ in public storage and test screenshots to
test-screenshots/. Data access is async; the base class keeps its
structure. Posts are created per run with the storage and the social
clients they post through.

Tests cover the base class (posting once per data time per client,
skipping clients that already posted, a failing client not blocking the
other, image-only mode) and the schedule, Salmon Run, new stage, and
Splatfest posts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 19:02:03 -07:00
Matt Isenhower
abb11e972d Revert "Return a run summary from each updater"
This reverts commit 49ee02731c.
2026-09-06 18:04:42 -07:00
Matt Isenhower
49ee02731c Return a run summary from each updater
Updater.update() now returns what happened: per-phase timings (fetch,
localize, process, publish, images), which languages had to be fetched,
which locale documents changed, and how many images were downloaded.
Subclasses add their own details (stages seeded and new stages, festival
rankings fetched). updateAll() carries each summary in its results, so the
Worker's "Updaters finished" log entry contains the whole picture per
updater. FilesystemStorage.writeJson now skips unchanged content like
BucketStorage does, so "locale documents written" means the same locally.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 18:04:55 -07:00
Matt Isenhower
e5c8032d06 Add a list endpoint to the updater Worker
GET /list?prefix= returns the keys under a prefix in the public bucket.
Wrangler can fetch, put, and delete single objects but not list them,
and this is the only way to inspect the shadow bucket from a terminal.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 11:59:31 -07:00
Matt Isenhower
960076faae Embed the CDN-missing image so an empty bucket seeds itself
One skill image is no longer served by SplatNet's CDN. The updater's
image step falls back to a bundled copy: under Node, cdn.node.js reads
the PNG from the repo; in the Worker, cdn.worker.js imports it as bytes
through a wrangler "Data" module rule. A "#cdn-images" import condition
in package.json selects the right one, so the PNG in src/common/cdn stays
the only artifact and nothing is generated or copied by hand.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 11:42:49 -07:00
Matt Isenhower
188d9cb504 Run the updaters in a Cloudflare Worker
workers/updater runs src/app/updater, the same code npm run splatnet runs
locally, against the Worker's R2 bindings through BucketStorage. The
Scheduler Durable Object from the proof of concept owns when the run
happens: an alarm at :00:10 that re-arms itself, with retries after a
failed run; the only cron trigger is a minute-30 watchdog that re-arms a
lost alarm. Operator endpoints (/run, /wake, /arm, /status) are behind a
bearer token. Handlers and the Durable Object are wrapped with
@sentry/cloudflare so the updaters' @sentry/core reports are delivered
when SENTRY_DSN is set.

updateAll() now returns a per-updater result (name, ok, duration, error)
that both the local runner and the Worker can report on, and the SplatNet
client reads its user agent lazily since Workers populate process.env
from the bindings.

Deployed in shadow mode against the dev bucket: a full run of all eight
updaters on Cloudflare matched production's data semantically within the
same hour, and steady-state runs take about seven seconds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 11:23:49 -07:00
Matt Isenhower
000f21e0c3 Make the whole package an ES module
Replaces the four nested package.json scopes with "type": "module" at
the root. The three CommonJS config files become .cjs. Webpack would
otherwise treat src/web as strict ESM, which forbids the require() calls
used for locale files and image assets and demands extensions on every
import, so the Vue config keeps the js rule in webpack's mixed mode.
Build, tests, lint, and the built site were verified, including the
dialogs, a language switch, and the screenshots page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 11:07:10 -07:00
Matt Isenhower
62e6f3aceb Remove the Docker container build and deployment
The updaters and posters are moving to Cloudflare Workers, and the
container will not receive the new code, so the image build workflow,
Dockerfile, Compose files, and Watchtower configuration go. The
container currently in production keeps running its last image until it
is retired.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 10:50:12 -07:00
Matt Isenhower
a9a7cff7d6 Remove the S3 frontend deploy job
The site is served by Cloudflare Pages now, so the job that synced the
built dist/ to the public bucket is no longer used.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 10:49:10 -07:00
Matt Isenhower
f38f5f48be Remove the S3 syncer
It existed to copy dist/ and storage/ between the container and object
storage. Writes now go to storage directly (a directory locally, R2
bindings in the Worker), and a fresh npm run splatnet rebuilds every
public file from SplatNet, so nothing needs a download either. Drops the
sync scripts, the AWS S3 variables from .env.example, and the
@aws-sdk/client-s3, s3-sync-client, and mime-types dependencies.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 10:46:08 -07:00
Matt Isenhower
8827f59b44 Declare prop types and clear the remaining web lint warnings
Every component now declares its prop types (vue/require-prop-types),
which was the bulk of what was left. The rest are one-off fixes:

- two <template> wrappers with no directive are removed (no-lone-template);
- the region dropdown's loop variable no longer shadows the region prop
  (no-template-shadow);
- the Splatfest winner bar renders the winning team's name as a real
  <span> inside the translated sentence instead of injecting markup with
  v-html (no-v-html).

The lint workflow's --max-warnings 0 now passes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 10:36:51 -07:00
Matt Isenhower
be6c23788f Keep the S3 sync and filesystem storage out of the updater import graph
updateAll() only runs the updaters now. The local (Node) flavour, which
also uploads to S3 when configured, lives in src/app/local.js next to the
filesystem storage wiring, and the entry points call that. The storage
barrel no longer re-exports FilesystemStorage. With both gone, the
updater tree bundles for a non-Node target with no unresolved imports.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 10:31:09 -07:00
Matt Isenhower
899b9ff9ea Move getTopOfCurrentHour out of utilities.js
utilities.js is the filesystem helper module the posters still use, and
the schedules updater only needed the time helper from it. With the
helper in its own module the updater import graph no longer pulls in
node:fs, which is what the Worker bundle needs. Also applies the
remaining ESLint fixes outside src/web.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 10:30:11 -07:00
Matt Isenhower
e3e773af4d Add lint workflow and lint-fix script
Copied from splatoon3.ink: lint on pushes to main and on pull requests,
failing on any warning, and npm run lint-fix to apply fixes locally.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 10:30:11 -07:00
Matt Isenhower
41dcff1709 Report updater errors through @sentry/core
The updaters run in the container today and in a Worker next, and
@sentry/node cannot be bundled into a Worker. Both @sentry/node and
@sentry/cloudflare are built on @sentry/core, where captureException
lives, so the updater base class imports from core and lands on whichever
client the entry point initialized. The Node entry point keeps
initializing @sentry/node; the Worker will use @sentry/cloudflare.

The two packages must stay on the same version: Sentry keys its global
carrier by version, and a split silently drops events. A test pins the
routing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 10:23:35 -07:00
Matt Isenhower
67ce13ef9f Apply ESLint fixes to the web code
Formatting only (template indentation, attribute order, self-closing
tags, semicolons, trailing commas), from `eslint --fix src/web`. These
rules come from vue2-recommended, which the previous config also extended;
vue-cli-service lint used to apply them silently on save.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 10:20:29 -07:00
Matt Isenhower
ea3639ea99 Upgrade to ESLint 10 and import bundled JSON with import attributes
ESLint moves to a flat config (eslint.config.mjs) with the same rules,
run directly by npm run lint; @vue/cli-plugin-eslint is dropped since the
Vue build does not need it. With a parser that understands import
attributes, the bundled data files (brands, skills, gear, finalFest,
shiftyStations) are imported with `with { type: 'json' }`, which Node and
the Workers bundler both support, instead of being read through node:fs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 09:59:09 -07:00
Matt Isenhower
19d1fb5d3d Move updaters onto public and private storage
Updaters take a storage object instead of touching the filesystem: a
publicStorage (what the site serves: data/, assets/) and a privateStorage
(updater state). Two implementations share one small interface
(exists / readJson / writeJson / readBytes / writeText / writeBytes):

- FilesystemStorage over a directory, used by npm run splatnet, so dist/
  and storage/ end up exactly as they always have and the S3 sync is
  untouched;
- BucketStorage over an R2 bucket binding (MemoryBucket in tests), with a
  thin per-run cache: exists() lists a directory once and answers from
  memory, readJson() parses a document once, and writeJson() skips the put
  when the content is unchanged.

The updater base class and localization processor keep their original
structure with awaits added; the processor reads its document lazily and
writes once per update, as splatoon3.ink's does. Objects written to a
bucket carry the content type and cache-control the S3 sync used to apply.

Tests cover the storage contract (run against both implementations), the
BucketStorage cache behaviour, MemoryBucket's R2-style listing, the
localization processor, the updater base, and each concrete updater.
scripts/compare-data.mjs compares two data directories ignoring SplatNet's
random ordering and ICS timestamps.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 09:50:14 -07:00
Matt Isenhower
2c636257f3 Replace axios with fetch
The SplatNet client now uses the global fetch, matching splatoon3.ink.
Non-2xx responses throw with the status. getImage returns a Uint8Array
and getText serves the language-file utility, which no longer reaches
into the HTTP client. updateGear fetches Inkipedia directly. axios is
removed; the web build never used it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 19:58:35 -07:00
Matt Isenhower
6a4ad0e0b0 Convert Node-side code to ES modules
Scope ESM to src/app, src/common, src/utility, and test with nested
package.json files, keeping .js filenames and leaving the web build alone.
regions.esm.js becomes regions.js. make-runnable is replaced by a small
action runner and module-alias by relative imports; both are removed.
npm scripts point at explicit entry files.

Output verified identical to the CommonJS run (differences are SplatNet's
own key and array ordering), and the Vue build and tests pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 19:50:05 -07:00
Matt Isenhower
477ca7f4ac Match Splatoon 3 directory browser styling
Some checks failed
Deploy / deploy-frontend (push) Has been cancelled
Deploy / deploy-backend (push) Has been cancelled
The initial Worker used a simplified renderer that omitted the established layout styles and folder/file icon markup.
2026-09-02 20:42:48 -07:00
Matt Isenhower
d1a4c6a531 Add R2 directory index Worker 2026-09-02 20:22:18 -07:00
Matt Isenhower
acd77f5b82 Revert "Configure Workers static asset deployment"
This reverts commit d3b51ec4b9.
2026-09-02 19:45:20 -07:00
Matt Isenhower
d3b51ec4b9 Configure Workers static asset deployment 2026-09-02 15:21:21 -07:00
Matt Isenhower
4158c4479f Merge pull request #59 from misenhower/codex/split-public-private-storage
Split public and private storage credentials
2026-09-02 14:46:19 -07:00