Underground 2 Revived
Play Need for Speed: Underground 2 (2004) in Chrome and Firefox with WebGPU graphics, Web Audio, up to 120 fps widescreen, controller support, free roam and Online races with friends. Fan project, not
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기TBC Raid Group Optimizer — Drag-and-drop party builder with buff planning for WoW TBC raids
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Raidsmith was formerly called Party Planner (renamed in 4.4.1). Saved plans, share links, backup files and the pp_* storage keys are unchanged, so everything made under the old name still works.
A raid group planner for World of Warcraft Classic Era, TBC Anniversary and WoW Forever. Import a Raid-Helper sign-up list (or paste a roster), let the optimizer lay out the parties for buff and debuff coverage, adjust by drag and drop, then share a link, copy raid chat text, or export an MRT note.
Production: one Cloudflare Worker (worker/) serves the static site, the share-link API and the guild tracker at https://app.raidsmith.workers.dev (deployed by Cloudflare Workers Builds, see Deploy below).
When the domain changes, the host lives in two places. In index.html, faq.html and whats-new.html (each page has its own og:url): og:url, og:image and twitter:image. Replace app.raidsmith.workers.dev in those three files (one find-and-replace). In the tests the host is ONE constant, SITE_HOST in tests/site-host.js, which head-tests.js, site-pages-tests.js and deploy-tests.js read.
It is a static site with no build step and no npm dependencies: index.html (the planner), two static pages (faq.html, whats-new.html), app.css and eighteen plain <script src> files (fourteen under js/, the four rules data files under js/rules/), plus the Cloudflare Worker (worker/) that serves them and keeps the live share links (worker/share.js, D1 table share_links).
| Path | What it is |
|---|---|
index.html | Markup only. Loads app.css and the scripts below, in this order. |
faq.html, whats-new.html | Static pages under the same site header and footer: the 16 FAQ questions, and the full release list. Served as plain files at /faq.html and /whats-new.html (no rewrite); the header links of all three pages point at them. They load only js/util.js, js/changelog.js, js/site-pages.js (and js/storage.js for the FAQ). |
js/util.js | esc(), loaded first by every page. |
js/rules.js | Config, the rules compiler Rulesets.compile with its closed Rulesets.CONVENTIONS schema, the compiled rulesets (one per game version) and the version helpers. |
js/rules/ | The rules, as data: catalog.js (shared by every version: classes, specs, icons, abbreviations) and one file per game version (tbc.js, classic.js, forever.js: buffs, debuffs, weights, tuning numbers, raids). The source of truth, edited by hand; see Editing the rules. Loaded before js/rules.js. |
js/changelog.js | APP_VERSION and the release notes behind whats-new.html, and the helpers that remember which release was seen. |
js/state.js, import.js, storage.js, livesync.js | Plan state, importers and exporters, local storage, live link sync. |
js/optimizer.js, analysis.js | Optimizer.plan() (pure layout), scoring, buff resolution, coverage insights. |
js/changes.js | The roster change log: who a sign-up sync, a raid size change or a live update benched, seated or moved. |
js/render.js, modals.js, app.js | Rendering and wiring. Every plan change goes through commit() (reconcile, persist, render). |
js/site-pages.js | Wiring shared by all three pages: the "New" badge on the header's What's new link, the release list, the footer feedback link and the FAQ's tips reset. |
The planner page (index.html, drawn by js/render.js) reads top to bottom as: the plan head (plan name, one status chip, a context line, Undo, Redo, Share, More and the Optimize button); the next-step bar, which shows the first notice that needs you and its buttons; the Raid check strip (role counts, missing buffs and debuffs as icons, and a Coverage button that opens the full list); optional panels (Manual changes, Campfires on WoW Forever, Raid notes, Changes); then the body, with the "Not in a group" list on the left (everyone who is benched, tentative, late or unplaced, with filters and search) and the raid groups beside it, three and two to a row. It uses the shared Forge look from guild.css (classes gs-pb-*) at real pixel sizes, with no CSS zoom. On a phone the groups and the "Not in a group" list switch with a two-way control, and the head's buttons sit in a bottom bar.
.gitignore is a whitelist: a new tracked file needs a !path line there.
Everything except short and live links works from any static file server, for example:
python -m http.server 5173
Then open http://localhost:5173/. The /<version>/<id> links and /api/share are Worker features, so they do not work on this plain static server: run the Worker instead (see Guild tracker below), where they do.
npm test
tests/run-all.js runs every tests.js, *-tests.js and *-scenarios.js in tests/planner/ (Planner), tests/guild/ (guild app) and tests/site/ (deploy, headers, CSP, brand, layout) as its own Node process, up to three at a time (PP_TEST_JOBS=1 runs them one by one), and exits non-zero if any fails (about a minute). The slow suites (scenario-tests.js, boards-tests.js) are split into shards listed in tests/shards.js; running a suite file directly still runs it whole, and --shard i/n runs one slice. Run one suite with node tests/run-all.js <name-filter> or node tests/<area>/<suite>.js (from any folder). tests/load-app.js reads the script list from index.html, so the suites always test the files the page loads.
tests/planner/boards-tests.js builds the optimized board of every scenario in scenarios.js and of the seven Classic/Forever rosters in tests/classic-forever-fixtures.js, and compares each with the golden snapshot tests/boards-snapshot.json, listing who moved when a rule or weight edit changes a layout. A changed layout is accepted only on purpose, with node tests/planner/boards-tests.js --update, which rewrites the snapshot (read its git diff before committing it).
tests/planner/rules-data-tests.js guards the rules data files in js/rules/: it pins the buff and debuff ids per version in tests/rules-ids-snapshot.json (a rename or removal is accepted only on purpose, with node tests/planner/rules-data-tests.js --update), checks that every version still compiles into the shape the app reads and keeps the vocabulary the code relies on (types, known roles, classes and specs, finite weights, references that exist, spell ids, an icon, abbreviation and CSS class for every buff id, an abbreviation for every debuff id), that each conventions block holds exactly the keys of Rulesets.CONVENTIONS and that the keys the code reads (rules.rules.<name>) and that schema agree, and exercises Rulesets.compile on small synthetic data.
GitHub Actions runs npm test on every push and pull request (.github/workflows/test.yml). Cloudflare does not run the tests, so a green check on main is what to look at before deploying.
Every tuning number in conventions and every buff and debuff weight lives in the rules data files in js/rules/, one per game version, meant to be edited by hand:
| Game version | File |
|---|---|
| WoW TBC Anniversary | js/rules/tbc.js |
| WoW Classic Era (faction-aware: Horde shamans, Alliance paladins) | js/rules/classic.js |
| WoW Forever (beta) | js/rules/forever.js |
| All three: classes, roles, specs and their icons, the Raid-Helper maps, and the icon, abbreviation and CSS class of every buff and debuff id | js/rules/catalog.js |
A version file is one plain object literal, RulesData.<version> = { ... }, loaded before js/rules.js, which compiles it at load (Rulesets.compile). There is no build step: edit and reload. The comments in each file explain its numbers, and a trailing comment on a weight names the research note behind it (tasks/research-*.md) where there is one. The Ideal-comp builders (js/app.js) and the blessing catalog (js/analysis.js) are code, not data.
Buff entries (buffs: one per id, in display order)
| Field | Meaning | Allowed values |
|---|---|---|
name, desc | Name and tooltip text. | Strings. |
spellId | The max-rank spell, to check the entry against Wowhead. Not read by the optimizer. | A positive integer, unique within the version; null when unknown. |
buffType | The slot rule the optimizer applies. | totem (one per element per shaman), paladinAura (one per paladin), warriorShout (one per warrior), passive (always on while its provider is in the group). |
totemElement | The element a totem occupies. | air, earth, water, fire; totems only. |
cooldownTotem | Mana Tide: credited on top of the shaman's standing water totem instead of occupying the slot. | true; totems only. |
scope | Where it applies. | party (everything the optimizer places) or raid: reaches every group wherever its provider sits, so the scorer does not count it per group and spreadProviders does not spread for it (Forever, 2026-10-16: Battle Shout, Trueshot Aura, Moonkin Aura, Leader of the Pack, Windfury, Grace of Air, Strength of Earth). |
sourceClass, sourceSpec | Who provides it; sourceSpec only when the buff is talent-gated. | A class of the catalog; a spec of that class. |
preferSpec | A placement hint shown in the UI; the scorer ignores it. | A spec of sourceClass. |
targetRoles | The roles that gain from it. | Any of tank, healer, melee_dps, ranged_dps, caster_dps. |
priority | The order of a group's buff badges (higher first) for the group's most common role. | A number, or a map role -> number. |
weight | What the optimizer scores, see below. | { dps, mitigation, sustain }, each optional. |
How weight is applied. weight.dps is what a recipient gains. Its key is looked up on the recipient in this order: the exact CLASS:Spec (PALADIN:Retribution), then the bare class (WARLOCK), then the role (melee_dps); no match is 0. The number is then multiplied by the recipient's own DPS weight: specDpsWeight for its CLASS:Spec, else roleDpsWeight for its role (0.8 when the role is missing too), so the strongest DPS is placed where it gets buffed first. dps: {} is written out for a buff that is worth nothing to anyone's DPS. weight.mitigation is what a tank gains, keyed by the tank's CLASS:Spec with '*' as the fallback for any other tank (not multiplied by the DPS weight). weight.sustain is one number for the tank group's healers, counted per healer present (at most sustainHealers).
Debuff entries (debuffs)
| Field | Meaning | Allowed values |
|---|---|---|
name, desc, spellId, sourceClass, sourceSpec | As for buffs. | |
debuffType | Descriptive only. | armor, attackPower, magicVulnerability, physicalVulnerability, attackSpeed, hitChance, holyVulnerability, healOnHit, manaOnHit, other. |
targetRoles | Who profits; the Open-slot pricing has a group for each of these combinations. | ['melee_dps', 'ranged_dps'] (physical), ['ranged_dps'], ['caster_dps'], ['tank'], or all three DPS roles. |
targetSchool | Narrows casters to the specs that cast one school. | shadow, fire, frost, nature, arcane; only with targetRoles: ['caster_dps']. |
weight | The Open-slot tier: what suggesting a spec that brings the debuff is worth (tier x the DPS weight of the raid members it helps; for a tank debuff, tier x tank count x modeConfig.max_dps.mit x openSlots.tankDebuffScale), on the scale of weight.dps. It does not move the party layout. | A number. |
competesWith, supersededBy | Debuffs that share its slot, and the one that replaces it; a debuff is not worth suggesting when one of them is already covered. | Debuff ids of the same version. |
Sections of a version file
| Section | Meaning | Allowed values |
|---|---|---|
buffs, debuffs | The entries above. | Id -> entry. |
specDpsWeight | Expected DPS output per spec relative to the raid's top spec (1.0): the multiplier on every buff weight that spec receives. | CLASS:Spec -> number. |
roleDpsWeight | The same per role, for specs without an entry. | Role -> number. |
bestAirTotem, bestPaladinAura | The default air totem and paladin aura per recipient role. A reference table the tests read; the layout picks by value. | Role -> buff id. |
specAura | The aura a paladin spec runs when several are valued equally. | Paladin spec -> aura id. |
spreadProviders | Classes whose buff is a single point of failure: the last optimizer phase spreads them one per group. Absent in TBC. | [{ class, spec? }]; spec may be a list, none means every spec. |
factionLock | true: Shaman is Horde-only and Paladin Alliance-only (Classic Era); false or absent: both on both factions. | Boolean. |
tankAnchorOrder | The tank classes in the order they anchor the healer/tank group; other classes come after. | A list of classes. |
healerGroupDps | The DPS specs the reference comp parks in the healer group as its one overflow seat (no guest cost there). | [{ class, spec }]; [] for none. |
stackPenalty | The penalty for a second copy of a party-buff provider in one group (triangular; every matching entry adds). | [{ class, spec?, weight }]; [] for none. |
conventions | Every tuning number and flag, see below. | The closed key list of Rulesets.CONVENTIONS. |
raids | The version's raids in picker order, planning templates last. | Id -> { name, size, groups, tier }; ids are unique across versions. |
Conventions. conventions holds the scorer's tuning numbers and flags, modeConfig (the objective weights per optimizer mode: only max_dps is supported, the other three stay so old saved plans and share links validate) and openSlots (Open-slot pricing). Every version file lists every key: Rulesets.CONVENTIONS in js/rules.js is the closed list, and a missing, an unknown or a mistyped key stops the page at load and fails rules-data-tests.js, so a misspelled knob cannot be ignored. A number of 0 switches a cost or bonus off and a flag of false switches a rule off for that version; a count is a threshold, not a switch (tankGroupHealerMax and classCrowdFree say what 0 does there); healerCapOverFloor: null means no cap. The line above each key gives its unit, what it trades against and the boards-tests.js fixtures that moved when its value was changed. The numbers that stay in js/optimizer.js are mechanics (counts that define a rule, triangular sums, guards) and search limits (the score gain a refinement move must beat, iteration caps), not scoring weights.
Ids never change. Saved plans and share links store buff and debuff ids (manual totem overrides, debuff assignments), so tests/rules-ids-snapshot.json pins them per version. Do not rename or remove one; adding, renaming or removing an id fails rules-data-tests.js until you accept the new list on purpose with node tests/planner/rules-data-tests.js --update.
The edit loop
js/rules/<version>.js.node tests/planner/rules-data-tests.js checks the vocabulary, the references, the ids and the conventions keys.node tools/rules-report.js <tbc|classic|forever> prints what the optimizer will use: the buff-by-recipient weight matrix (the effective weight per CLASS:Spec), the mitigation and sustain weights, the debuffs, and every convention flat with the first clause of its comment (--json for scripts). Read-only.node tests/planner/boards-tests.js rebuilds the 121 fixture boards and lists who moved. A deliberate change is accepted with node tests/planner/boards-tests.js --update (read the diff of tests/boards-snapshot.json first).npm test runs everything.Classic and Forever are independent games: their files may differ on purpose and no test compares them, so edit one without touching the other.
The share links need none: they live in the Worker's D1 database (DB binding). The Worker's secrets are listed by name in .env.example.
One Cloudflare Worker serves everything (wrangler.jsonc): the static files from this folder (.assetsignore keeps tests, docs, the Worker source and secrets out), the API routes and the share links. There is no build step. run_worker_first sends the API paths and /tbc/*, /classic/*, /forever/* to the Worker, which answers the version paths with the front page (index.html) so a live link opens the planner and the page fetches the plan.
_headers is the ONLY place a Content-Security-Policy is set (tests/site/headers-tests.js fails on a second source). On every route it carries:
Content-Security-Policy (scripts and styles from the site, Google Fonts, connections only to the site, Raid-Helper and Wowhead's tooltip data, no inline scripts or styles; wow.zamimg.com is allowed for Wowhead's item tooltip script, stylesheet and icons, which only guild pages that show items load), X-Content-Type-Options: nosniff and Referrer-Policy: strict-origin-when-cross-origin.tests/site/deploy-tests.js checks .assetsignore can never exclude something the page loads.
The guild tracker (the shell in js/guild/ and guild.css, loaded by index.html, faq.html and whats-new.html; the Worker in worker/, D1 migrations in migrations/) runs on Cloudflare's Wrangler (wrangler.jsonc holds the live D1 id; local dev keeps preview_database_id local-raidsmith).
Run it. Node 22 or newer. Fill .env from .env.example (names only; .env is never committed or served), then:
npm run guild:migrate # applies migrations/ to the local D1
npm run guild:dev # Worker + local D1 + static files; the app is http://localhost:8787/
Both wrap tools/guild-dev.js, which runs npx --yes [email protected] with WRANGLER_SEND_METRICS=false and --persist-to ~/.raidsmith-dev/wrangler-state (outside OneDrive, so the SQLite files are not synced). Wrangler is pinned at 4.146.0 here and in the tool; a test checks the two agree. Extra arguments pass through: npm run guild:dev -- --port 8788. Nothing needs a Cloudflare login.
To use a different database (a fresh one for a check, say), pass --state <dir> or set RAIDSMITH_DEV_STATE=<dir>; --state wins, then a --persist-to you pass yourself, then the variable, then the default. Exactly one --persist-to reaches Wrangler. Keep that path short and write it with forward slashes (C:/Users/you/AppData/Local/Temp/rs-check): workerd fails with "internal error" on a long path such as a deep Temp folder (C:\Users\<you>\AppData\Local\Temp\rs-* works), because it cannot open its SQLite files there, and in Git Bash a backslash path loses its separators, so Wrangler creates a stray folder in the repo. The tool runs npm's npx-cli.js with Node directly (no shell), so a home directory with spaces works; if that script cannot be found it falls back to npx and quotes the arguments for Windows.
Without real Discord credentials. node tools/fake-discord.js starts a fake of Discord's OAuth and two API calls on 127.0.0.1:8790 (configure the fake user and servers with flags or FAKE_DISCORD_* variables). Point the Worker at it with npm run guild:dev -- --var DISCORD_API_BASE:http://127.0.0.1:8790 and put the fake user's id in GUILD_CREATOR_ALLOWLIST (also a --var; leave it out and any fake user who administers a server may create a guild for it). The Worker honours DISCORD_API_BASE only for https://discord.com or a local http://localhost:<port> / http://127.0.0.1:<port>, and it is never set in wrangler.jsonc. A second local Worker on another port (for example --port 8788) also needs --var DISCORD_REDIRECT_URI:http://localhost:<port>/auth/callback with that port; otherwise sign-in redirects back to localhost:8787.
Without real Raid-Helper or Warcraft Logs. node tools/fake-sources.js [--port 8791] [--server-id <Discord server id>] (or FAKE_SOURCES_PORT, FAKE_SOURCES_SERVER_ID) fakes the Raid-Helper v4 events API and the Warcraft Logs token + GraphQL endpoints with invented events and one invented report; its header comment lists the event ids and the report codes. --server-id is the Discord server the main event is posted in (default: the fake Discord's server 900000000000000001). Point the Worker at it with npm run guild:dev -- --var RAID_HELPER_API_BASE:http://127.0.0.1:8791 --var WCL_API_BASE:http://127.0.0.1:8791, and set any WCL_CLIENT_ID / WCL_CLIENT_SECRET (the fake accepts anything). Like DISCORD_API_BASE, both bases are honoured only for http://localhost:<port> / http://127.0.0.1:<port> and are never set in wrangler.jsonc. For the raid log (attendance) it also serves fights with friendlyPlayers and an owner on Fake000000000001 (6 boss pulls, one player late, one gone before the last pull), a second report of the same night (Fake000000000002, with a player who belongs to another team: the fill-in case), an older night (Fake000000000003) and the report list for the backfill (reports(...), two per page); the header comment of tools/fake-sources.js lists them. Attendance never stores a report, only who was in each boss pull; the backfill list shows report titles but does not keep them.
Battle.net (Phase 5). Members connect Battle.net from the avatar menu (Linked accounts), see their characters per guild (My characters) and claim the ones that are theirs; officers approve claims in the Review queue (Character claims group) and can import the guild's member list from Battle.net (Roster > Import). The Worker needs a Battle.net API client made at develop.battle.net: put BNET_CLIENT_ID, BNET_CLIENT_SECRET and BNET_REDIRECT_URI (the registered redirect, for example http://localhost:8787/auth/bnet/callback) in .env. Without them the pages say "Battle.net isn't set up on this server". The namespace per game version is one table in worker/battlenet.js (NAMESPACES; TBC Anniversary profile-classicann, Classic Era profile-classic1x, WoW Forever none yet, so its guilds show "Battle.net doesn't offer character data for WoW Forever yet"); the research and its sources are in that file's header comment. Blizzard's terms allow keeping Battle.net data for 30 days at most: a daily Cron Trigger (triggers.crons in wrangler.jsonc, worker/bnet-purge.js) clears character lists and deletes claims that old (the link itself, the Battle.net account number and BattleTag, is a sign-in identity since Phase 7a and stays while it is connected). The dev tool starts Wrangler with --test-scheduled, so locally you can run it by hand: curl "http://localhost:8787/__scheduled?cron=17+4+*+*+*" (use your port).
Without real Battle.net. node tools/fake-battlenet.js [--port 8792] (or FAKE_BATTLENET_PORT, FAKE_BATTLENET_ACCOUNT_ID, FAKE_BATTLENET_BATTLETAG) fakes Battle.net's OAuth (/authorize, /token, /userinfo), the account profile (/profile/user/wow, namespaces profile-classicann-{us,eu} and profile-classic1x-{us,eu}) and the guild roster (/data/wow/guild/:realm/:guild/roster, any realm and guild; the guild slugs missing and refused answer 404 and 403) with invented characters and members; its header comment lists everything. Point the Worker at it with npm run guild:dev -- --var BNET_API_BASE:http://127.0.0.1:8792 --var BNET_CLIENT_ID:x --var BNET_CLIENT_SECRET:y --var BNET_REDIRECT_URI:http://localhost:8787/auth/bnet/callback (with a second Worker port, use that port in the redirect URI). Like DISCORD_API_BASE, BNET_API_BASE is honoured only for http://localhost:<port> / http://127.0.0.1:<port> and is never set in wrangler.jsonc; one base serves both the OAuth host and the API host.
Checks against a seeded copy. node tools/guild-smoke.js up --name <x> [--port 8790-8799] [--with discord,sources,bnet] migrates a fresh state under ~/.raidsmith-dev/chk-<x>, starts the fakes and the Worker on 127.0.0.1 (never port 8787 or the live database) and seeds a guild through the real API: a Guild Master and two members, a team, three people with characters, a past raid built from the fake Warcraft Logs report and an upcoming raid. It prints { base, cookies, ids, ... } as JSON; run --name <x> <script.js> runs a Node script with those values (require('./tools/guild-smoke.js').fromEnv().api('gm', 'GET', route)) and down --name <x> stops everything and deletes the state. The header of tools/guild-smoke.js has the details.
The guild script list. js/guild/manifest.js is the one list of guild scripts in load order; index.html, faq.html and whats-new.html carry a single <script src="/js/guild/load.js">, which injects them in that order, and tests/guild-scripts.js reads the same file. A new guild page is one line in the manifest, next to the page it builds on.
Accounts (Phase 7a). A Raidsmith account (a users row) is the person who signs in; Discord and Battle.net are sign-ins linked to it. The sign-in page offers "Continue with Discord" and, where the server has Battle.net set up (GET /auth/providers), "Continue with Battle.net": an identity nobody holds makes a new account, a known one signs into its account, and every sign-in starts a new session. Linked accounts (avatar menu) connects and disconnects each one (/auth/login?mode=link, /auth/bnet/login, DELETE /auth/discord, DELETE /auth/bnet) and sets the display name (PATCH /api/me); an identity that already belongs to another account is refused (accounts are never merged), and the last remaining one cannot be disconnected. Each flow has its own signed state (worker/session.js). A member of a guild is anyone not blocked who is in its Discord server or whose account is linked to one of its roster people: that rule is worker/membership.js, and every route builds its SQL from it. An officer links an account on the person page (PUT /api/guild/:gid/roster/:pid/account; the look-up GET /api/guild/:gid/accounts?q= answers only for accounts that sent a character claim or whose whole BattleTag or display name is q, and q only for guild-wide roster.edit: a Team Lead sees the claimants and may link only an account that is already a member or has a claim waiting). A different Battle.net or Discord account never replaces a linked one (disconnect first), and disconnecting Discord deletes its ID and nickname from roster people and sign-ups. Deploy the Worker and apply 0012 together: the old Worker's sign-in does not know account_key. Migration 0012_accounts.sql does not rebuild users: DROP TABLE users would run the ON DELETE actions of every table that references it, and D1 cannot switch foreign keys off, so the old discord_id column became account_key and a new nullable discord_id took over; its header comment and guild-accounts-worker-tests.js show why. Run npm run guild:migrate once after pulling it. To try a Battle.net-only sign-up locally, run tools/fake-battlenet.js as above and use the sign-in page's Battle.net button.
Future enhancements and the idea box. The roadmap page (#/roadmap, signed in or out) draws js/guild/roadmap-data.js: add an item there (the fields are documented at its top) to put it on the page. Signed-in people also get a Suggest an idea form; the ideas are app-wide rows in roadmap_ideas (migration 0007). Triage them from the terminal against the same local D1 the dev server uses (--state <dir> and RAIDSMITH_DEV_STATE work as above; it runs tools/guild-dev.js execute, which is always --local):
node tools/roadmap-ideas.js list [--status new]
node tools/roadmap-ideas.js mark <ideaId> <added|declined|done> [--roadmap <itemId>] [--note "..."]
list prints JSON, newest first. mark changes one idea and prints it; --roadmap must be an id in roadmap-data.js (add the item first). Run npm run guild:migrate once after pulling migration 0007.
Native sign-ups and the raid schedule (migration 0010). Members sign themselves up for an upcoming raid of a team they have a character on (Raids list: one-click Sign up; raid page: the Sign-ups panel with Signed, Late, Tentative and Can't come, a character, a spec and a note) until the raid starts or an officer closes sign-ups; officers and team leads edit any sign-up. A Raidsmith sign-up is a raid_signups row with source = 'raidsmith' and replaces the person's Raid-Helper row in the list, the counts, the Planner seed and the attendance, so nothing is counted twice. Guild settings > Raid schedule (anyone with raids.edit; a Team Lead opens it from the Raids page) holds a team's weekly slots; Create upcoming raids fills the next 1 to 8 weeks and can be left to run by itself when an officer opens Raids (no cron). The routes are listed under "API (as built)" in tasks/reference/guild-phase6-signups-spec.md. Run npm run guild:migrate once after pulling migration 0010; no new variables or fakes are needed (the fakes above cover Raid-Helper events that sit beside native sign-ups). An event on another day. Linking, replacing or refreshing the Raid-Helper event of a raid that has no log, when the event is on another guild-local day than the raid, answers 409 event_other_day (eventStartsAt, eventDate, raidDate) and changes nothing; the pages ask "Move the raid to that day?" (Move raid / Cancel) and send the call again with moveRaid: true, which sets BOTH starts_at and raid_on (a team has one raid per day, so a day it already has a raid on is 409 raid_date_taken). A raid with a log keeps its date; the same day behaves as before (an upcoming raid follows the event's start time).
Officer tools (PM wave 1, migration 0013). Five features for the people who run a guild, all on the existing Worker with no new variables or fakes: (1) Activity log - Guild settings > Activity (Guild Master only, audit.view): GET /api/guild/:gid/audit?type=&actor=&from=&to=&before= reads audit_log newest first, 50 a page, with a plain sentence per change built from live names (worker/audit-view.js; every audited action in worker/audit.js needs a group there, a test fails until it has one), a before/after toggle, and &format=csv for up to 5,000 rows (formula-safe, UTF-8 BOM, CRLF). Note text is never in the log. (2) Watch list - the attendance policy has watch: { below: 0.75, dropBy: 0.15 } (fractions, null = off, per guild or team, read live so no raid goes stale); the team attendance page shows who is below the line or fell by dropBy against the previous window of the same length (watchList in worker/attendance-metrics.js; a fall is only judged when the previous window counted 2+ raids; reserves and inactive people are left out). (3) Needs attention - the guild home #/g/:gid for anyone holding review, roster, raid or loot editing (everyone else gets the member Home on the same address: next raid with their own line-up seat, attendance, last loot, away notices, calendar; read from existing routes for me.personId, which the guild payload carries): GET /api/guild/:gid/attention reads everything in one batch and returns the open items (raids to review, unmatched loot winners and sign-ups, pending claims, trials ending within 7 days, watch list, raids with no loot import in the last 14 days, upcoming raids under the team size, sign-ups still open 60 minutes before the start as information) at most 10 per kind. Home redesign, round 2 (migration 0024, attention_dismissals): a to-do can be dismissed or snoozed for EVERY officer (POST /attention/dismissals { itemId }, DELETE /attention/dismissals/:itemid = Restore; the server picks mode, snooze length and fingerprint from the kind, DISMISS_RULES in worker/attention.js: a trial snoozes 3 days, the Discord webhook 7, the rest hide until what they showed changes; audited as attention.dismiss / attention.restore), the payload adds hidden (who and when), counts per team (open watch entries, rank suggestions), week (every raid from yesterday to today + 5 with its readiness facts and a status word) and on the facts about a raid (low sign-ups, declined or unconfirmed seats, unlocked), which are not to-dos and not in . The watch list and rank suggestions left Home: Attendance marks a watch entry Reviewed (the team payload gives each entry , , ), and it returns after a further 5-point drop or a newer missed raid. (4) - feeds the card on a trial's person page; Promote, Extend and End use the existing person PATCH. (5) - Raids > Subscribe in your calendar: makes (or replaces) a private link for the signed-in person and team; the token is 32 random bytes and only its SHA-256 is stored, so the URL is shown once; turns it off; needs no cookie, re-checks at every fetch that the person is still in the guild's Discord server, not blocked and may see the team's raids (otherwise a plain 404), and serves with the team's raids from 30 days back to 60 ahead (cache 15 minutes). gained . Run once after pulling migration (the number leaves room for 0012, the accounts branch).
Discord roles, the role editor, guest links and the public page (Phase 7, migration 0014).
bot, permissions=0, built from DISCORD_CLIENT_ID and shown on Guild settings > Discord roles), it can do nothing there, and the Worker only READS with it: the server's roles (GET /guilds/{id}/roles) and one member's roles (GET /guilds/{id}/members/{user_id}; no privileged intent). Create the bot on the same Discord application as the sign-in and put its token in DISCORD_BOT_TOKEN (.env locally, a platform secret later; never committed, served, logged or returned). Leave it empty and the mapping is off, nothing else changes. The token goes only to https://discord.com or the local fake (DISCORD_API_BASE rule above).@everyone is not offered). The mapping is applied at every Discord sign-in (and when Discord is linked), by Sync now (one person, People and roles) and by Sync everyone who signed in (25 people a call, repeated by the page). The sync only writes assignments marked discord; a role an officer set by hand is a separate manual row it never touches, and a role held both ways shows once, as the person's own (manual wins). When Discord cannot be read (no token, bot not in the server, bad token, rate limit, outage) nothing changes and sign-in still works; a person who left the server loses what Discord gave; blocked people and the Guild Master are skipped. Removing a mapping takes what it gave at once; a new mapping applies at each person's next sign-in or Sync now.worker/permissions.js ACTION_INFO (only scopes the routes honour; guild.roles, guild.access and audit.view are the Officer role's alone; Guest and Public are read-only; a custom role ranks as Member in the hierarchy), and nobody hands out a role that holds more than they hold. A role's permissions are read from the effective_role_permissions view, never from role_permissions directly.Recruiting and the WoW Forever launch (PM wave 3, migration 0017). Five features, no new variables or fakes. (1) Intent board - Roster > Intent: an officer opens a round per team, members pick a main (class and spec; the role follows from the spec) and an optional alt with a note of up to 120 characters, the board adds the picks up by role and class against the team's size and role targets, and reads which raid buffs and debuffs the picked roster covers with the Planner's own rules (rosterBuffCoverage in js/analysis.js, laid out by Optimizer.plan and read with the coverage rail's functions, no Planner state touched; wave3-planner-tests.js compares it with the Planner's own functions). (2) Recruiting - Roster > Recruiting: Raider and Trial mains of the team by role against officer-set targets (defaults 25: 3/6/8/8, 20: 2/5/7/6, 10: 2/3/3/2, 40: 4/10/13/13; teams.recruit_targets), the open intent round laid over them, the missing buffs and a plain-text recruiting post to copy. (3) Applications - Guild settings > Applications (guild.teams) switches #/apply/<guild id> on and holds up to 8 questions; the form works signed out (a signed-in applicant's account is kept), is limited to 3 an hour per browser (a cookie the form's GET hands out) and 20 a day per guild with a honeypot field, and lands in Roster > Applications (roster.edit), where Accept makes a Trial person (14 days unless set) with the character as Main and links the account; Needs attention shows new ones and Delete my data removes the applicant's own. /api/apply/* is in run_worker_first. (4) CSV import - Roster > Import > CSV (worker/csv-roster.js: header found by itself, ThatsMyBIS character exports accepted, an owner column groups a person's characters, at most 500 rows). (5) Default team size - a new team starts at the version's usual size (TBC 25, Classic 40, WoW Forever 20, DEFAULT_TEAM_SIZES in worker/raid-zones.js). Tests: guild-wave3-worker-tests.js, guild-wave3-ui-tests.js, guild-wave3-contract-tests.js, wave3-planner-tests.js; the isolation walker also covers the open apply routes.
Discord posts and automatic refresh (PM wave 4, migration 0018). Four features, no new variables (a webhook is validated against DISCORD_API_BASE: discord.com, or the local fake). (1) One scheduler - wrangler.jsonc has a second Cron Trigger, */15 * * * *; worker/scheduler.js picks the work by the event's cron string (the daily one still runs the Battle.net purge and now prunes notification_log after 90 days). Each 15-minute run is bounded (50 guilds, 200 messages, 25 Raid-Helper asks), nearest raids first, and every message is once-only: its notification_log row (dedupe key per raid and kind) is claimed before the post goes out; a transient failure (429, 5xx, no answer) is retried by the next runs up to 3 attempts, a body Discord refuses is final. Locally: curl "http://127.0.0.1:<port>/__scheduled?cron=*/15+*+*+*+*". (2) Channel posts - Guild settings > Notifications (guild.teams): per team a Discord channel webhook (stored, never sent back: the page shows .../<last 4>), an optional role to mention, and five switches, all off by default: the roster when published, loot after an import (both posted by the route that makes them), raid reminders 24 h and 2 h before, a reminder for people who have not answered (N = 6/12/24/48 h before the sign-up deadline) and one for rostered people who have not confirmed (6 h before; the count shows in Needs attention from the same moment, with or without a webhook). Posts are plain embeds with a link to the raid and allowed_mentions that can only ping the typed role and the people a reminder names by Discord ID, never @everyone; a person who ticked "Don't mention me in reminders" on Linked accounts (PUT /auth/reminders, table reminder_optouts, goes with Delete my data) is listed by name only. A webhook Discord answers 404/401/403 is switched off at once (audit row "Raidsmith"), shows in Needs attention and delivers the messages it missed once a new address is saved. DMs need a gateway bot and are not built. (3) Automatic sign-up refresh - worker/event-refresh.js asks Raid-Helper about every upcoming raid with an event every 12 h, every 2 h inside the last 48 h before the start, and replaces the Raid-Helper sign-ups exactly as an officer's refresh does (eventLinkStatements in worker/routes/raids.js); it never moves a raid: an event on another day sets raids.event_moved_on, which Needs attention and the Raids page show until an officer settles it with the existing 409 / moveRaid flow. The Raids page says "Sign-ups refreshed 12 min ago". (4) Checks - fake-discord.js records webhook posts (, ); triggers the 15-minute run on an isolated server and reads them back. Run once after pulling migration . Tests: , , .
Loot board and manual awards (PM wave 5, migration 0019). (1) Pasted lists - on any raid, upcoming or past, an officer (loot.edit) picks Actions > "Loot prio (TMB)" or "Soft-reserves (softres)" and pastes the export text (or the saved file). The browser decodes it (js/guild/tmb.js, softres.js: base64, then zlib via DecompressionStream('deflate'), then JSON; the format facts and the Gargul source commit are in each file's header) and PUT /api/guild/:gid/raids/:rid/loot-lists { source: 'tmb' | 'softres', rows } stores the rows in raid_loot_lists; a paste replaces that source's rows, an empty list clears it. A TMB export has no received flag and no item names, so "already got it" comes from Gargul awards on earlier raids of the same team and names from the guild's past awards (else "Item 12345"). (2) Loot board - GET /api/guild/:gid/raids/:rid/loot-board (whoever may see the team's loot in full) builds one row per item with someone in line: TMB prio people on the raid's roster (signed up, placed, present, else everyone on the team) by order, then soft-reserve holders (guests and pugs included), then wishlists. A roll-off is two or more different people at the top: the best prio tier plus every soft-reserve holder who has not got it yet. After the raid each Gargul award is compared with the line (prio #1, lower, elsewhere, disenchanted). Rules in worker/loot-board.js; the card has filters and a Print button (it fills the page's #print-sheet). (3) Manual awards - Loot card > Add award, or Actions > Add loot award: POST /api/guild/:gid/raids/:rid/loot/manual { itemId, itemName?, winnerName?, winnerRealm?, tag?, note? } on a raid that has started. loot_awards.source is CHECK (source IN ('gargul')) and the table was rebuilt once (0011), so a hand-made award keeps source = 'gargul' and has manual = 1 (an ADD COLUMN) and a checksum m-<uuid> that no Gargul export can produce; it counts in the balance, a winner who is not on the roster goes the usual Review queue way. Run npm run guild:migrate once after pulling migration 0019. Tests: guild-wave5-parser-tests.js, guild-wave5-worker-tests.js, guild-wave5-ui-tests.js, guild-wave5-contract-tests.js.
Data out, progress and rank ladders (PM wave 7, migration 0021; 0020 is unused). (1) Read-only API keys - Guild settings > Data access (Guild Master and guild.access): a key (name, one team or the whole guild, any of roster / attendance / loot / raids, 30 / 90 / 365 days or never) opens GET /api/v1/<dataset>.csv?key=rsk_... (for Google Sheets =IMPORTDATA("...")) and GET /api/v1/<dataset>.json with Authorization: Bearer rsk_... (JSON never takes the key in the address). The token (rsk_ + 32 random bytes) is shown once and only its SHA-256 is stored (api_keys, like guest links); a key reads what the built-in Member role can read for its scope (resolveKeyPermissions, so the guild's edits to Member apply at every request) minus notes and Discord / account data (worker/api-data.js names every column; loot private to each player reads as nothing), at most 20 working keys, 60 requests an hour per key (429 with Retry-After), CSV per RFC 4180 with a leading apostrophe on text cells that start with =, +, - or @ (worker/csv.js). run_worker_first gained /api/v1/*; guild-isolation-tests.js classifies the open route and walks the key routes. (2) Team progress board - Raids > Progress: GET /api/guild/:gid/teams/:tid/progress builds, per zone in the game version's order, each boss pulled with its first kill date, the wipes before it, the kills since (a second uploader's copy of a fight counts once) and the nights with a kill (first pull to last kill; the best clear is the quickest night among those that killed the most bosses), only from raid_pulls (worker/progress.js). The public page has a separate "Progress" section (a guest link still shows its four sections). (3) Rank progression - the attendance policy has a guild-only ladder: { ranks: [{ id, name, rules }] } (2 to 6 ranks, lowest first; rules minAttendance, minRaids, minWeeksAttended, minWeeksSinceJoin, maxMissed, since: 'join' | 'rank'; no rules = given by hand only), edited in Attendance settings; players.rank / rank_on hold the label (NULL = the first rank), separate from the roster status. worker/rank-ladder.js suggests "promote to " (its rules all met) or "review (below )" (the current rank's attendance or miss rules failed over 3+ counted raids) for raiders and trials, judged on the team of their Main; the team Attendance page, the person's attendance page and Needs attention show them, an officer () accepts with one click (, audited as ) or dismisses for 30 days (), and sets a rank by hand on the person's page. The Vindicated tracker's ladder (Member -> Raider after 2 attended raid weeks -> Core Raider after 8 attended nights since the promotion) is a test fixture. Run once after pulling migration .
WoW Forever launch readiness (PM wave 8, migrations 0022 and 0023). Five features, no new variables or fakes. (1) Two-part names - a Forever guild takes a character name of one or two words, each 2 to 12 letters, one space (cleanCharacterName(raw, gameVersion) in worker/roster-data.js; name_key is the lower-cased full name with the space); every other version stays one word. A one-word name that comes in (a log, a Gargul winner, softres / TMB rows, a CSV, a plan) resolves to the guild's ONE character that has it as its first word - after the full name is tried - and two candidates leave it unmatched for review (worker/name-resolve.js, applied when the name is read, so everything stored already matches by exact key). The Planner needed no change except the plain-text roster reader; its share code, MRT export and chat text are pinned with spaced names in wave8-planner-tests.js. (2) Raid duties - Actions > Set raid duties: Raid leader, Loot master, Logger, Plan maker and one free-text duty per raid (raid_duties, GET / PUT /raids/:rid/duties), shown on the raid header and as a "Duties" field of the Discord roster post. A raid whose Logger is named, that has no Warcraft Logs report and is not hand-entered shows "Upload the log for Tue Oct 21" in Needs attention 5 hours after its start (a 3 hour raid and 2 hours of grace, logDueAt in worker/duties.js; for 7 days) and, when the team webhook is on, one post naming the Logger (notification_log kind log_nudge, once per raid). (3) Launch funnel - Roster > Launch for a Forever guild, or for a team an officer marks (Guild settings > Teams, teams.launch_team): per person intent picked (the intent board), character created (a character of the picked class on the team), level band (1-19, 20-39, 40-59, 60+; typed, else the level of an approved Battle.net claim) and a ready tick, with counts per stage and role and the gap to the raid size (worker/launch-data.js, GET /launch, PUT /launch/:pid, PUT /teams/:tid/launch). (4) Guest sign-up link - Actions > Guest sign-up link on an upcoming raid makes #/join/<token> (token shown once, SHA-256 stored in raid_join_links, works until the raid starts, until turned off, and for N guests): a guest with no account gives a name, class, spec and note and becomes a native sign-up with raid_signups.guest = 1 and no person (listed as Guest, in the Planner seed, counted, never an attendance row); 3 sign-ups an hour per browser cookie, an optional per-raid guest limit that an officer sets (, migration ; none by default), a honeypot field, and a character on the roster is told to sign in. The two open routes and are in and classified in . (5) - Actions > Paste raid roster on a raid with no report: names, /who or an addon's raid frame (), matched like a log's names; Check names, then Save attendance sets the hand-entered attendance, and the names that matched nobody wait in the review queue like unmatched log names (; a name that later gets a person counts that person present). Migrations and rebuild two LEAF tables (nothing references them; D1 cannot rebuild the rest): for the new kind (0022) and for a 25-character name (0023, its own file so a database without 0019 can take 0022). Run once after pulling them. Tests: , , , and the isolation suite.
Design framework, wave 1 (raid page and Guild settings; no migration). (1) Raid page (js/guild/pages-raid.js, T2 in js/guild/ui-templates.js) - a stage strip (Sign-ups, Plan, Publish, Log, Loot) worked out from the raid's own data (stagesOf), the next-step bar with the page's one primary for the current stage (nextStepOf: Plan in Planner, Publish line-up, Add log, Review attendance, Import loot awards; a viewer who cannot take the step reads who is waited for), the viewer's own sign-up or result as one "You" line, then Sign-ups, Plan and line-up, Loot prios, After the raid (locked until the start). After the start the page leads with the log and the people list (Needs review by default; Everyone leaves out people who joined after the raid day unless they signed up, were in the log or were decided on: people[].joinedAfter from loadRaidDetail), then the loot awards, then Plan, line-up and sign-ups as summaries; Add a sign-up, Close sign-ups and the status form are gone. More has Open in Planner first and Delete raid alone at the bottom; the plan's name opens the Planner (its menu is Plan settings). The Publish line-up question says who is told (discordPost in the officer's GET .../roster reply: a usable team webhook with line-up posts on). (2) Guild settings (T3) - five groups (Guild, People and access, Sharing, Raid rules, Connections and data) in SETTINGS_TABS (js/guild/shell.js), a list on the left and one page at a time (on a phone the list is the first screen: the bare address stays on it; a wide screen opens the first page); every old address works and accounts reads as people. Form pages hold edits behind the sticky Save bar (Save / Discard, "Saved at 8:14 PM CT" in the page head; pages that keep a draft in state count it with ui.saveBar.draftChanges); Attendance is three sections with an "On this page" list. General (new) - PATCH /api/guild/:gid { name?, realm?, timeZone? } is the Guild Master's alone (403 for everyone else, including officers), validated like create (name 1-60, a realm guild's realm 1-64 with the listed spelling and slug, an IANA zone; the game version, region, Discord server and a Forever ruleset are refused as fixed) and audited once as guild.update with only the changed values; the Guild Master's guild payload also carries discordServerId, shown read-only. Tests: guild-fw1a-ui-tests.js, guild-general-worker-tests.js, the route checks at the end of guild-shell-tests.js, and two checks in guild-wave2-worker-tests.js.
Raid plans in the database (migration 0027). A guild raid's Planner plan lives in the database (raid_plan_doc, worker/routes/plan-doc.js): opening it from the raid page reads it, every change autosaves to it (js/plansync.js), and officers editing the same raid see each other's changes through live updates ("Updated by Kim" with Undo). A live link (/api/share) is only for sharing a plan outside the guild; the raid does not need one. The standalone Planner, with no guild or no raid, is unchanged: plans stay in the browser and short links work as before.
A local-only switcher so the Guild Master can see every page with another role's rights. Turn it on for one run: node tools/guild-dev.js dev --var DEV_VIEW_AS:1 (or DEV_VIEW_AS=1 in a local .env; never in wrangler.jsonc, a test fails if it is there). It works only when the flag is 1 AND the request host is 127.0.0.1 or localhost; otherwise every /api/dev/* route answers the plain 404 and the cookie is ignored. Only the REAL Guild Master of a guild sees the bar (gd.dev in GET /api/guild/:gid) and may use the routes. The bar sets the __Host-rs_viewas cookie (PUT /api/dev/view-as { gid, roleId, teamId? }, DELETE to clear), and loadGuildContext then re-resolves can, roles, rank and isGuildMaster from ONE stand-in assignment (Officer, Team Lead for a team, Member, or a custom role), so a viewed Officer loses the Guild Master's own powers; a stale role or team falls back to the real rights. Writes follow the stand-in rights and the audit log keeps your real user. "Guest link (preview)" and "Public page (preview)" (GET /api/dev/guild/:gid/viewer?role=guest|public, at #/dev/viewer/<gid>/guest|public) draw the same read-only views a visitor gets, even while the public page is off. Tests: guild-viewas-worker-tests.js, guild-viewas-ui-tests.js.
Limits under wrangler dev. The short links (/api/share, /tbc/:id, /classic/:id, /forever/:id) are Worker routes and work on 127.0.0.1:8787 too (without Cloudflare's CF-Connecting-IP header the rate limit does not apply locally). The planner pages, /faq, /whats-new and the guild tracker work. The guild tracker is part of the app shell at / (guild pages are /#/g/<id>/roster and so on); the old /guild address redirects to /#/.
What Wrangler 4.146.0 does here (checked 2026-10-14).
main inside assets.directory: "." is accepted with no error or warning..env, .wrangler/, node_modules/ or worker/ from the assets: without .assetsignore every one of them is served. .assetsignore hides them, and guild-config-tests.js pins its patterns._headers and .assetsignore are never served themselves; _headers applies to the assets (the only CSP; it allows the Discord CDN for avatars).html_handling serves /faq from faq.html (/faq.html redirects to /faq), so the help pages link to /faq.html and /whats-new.html and the browser lands on /faq and /whats-new.--var KEY:VALUE overrides a value from .env, and dev --env-file <path> exists.d1 migrations apply --local and dev run with no Cloudflare login.run_worker_first routes (/guild, /api/guild, /api/guild/*, /api/roadmap/*, /api/me, /api/guest, /api/public/*, /api/v1/*, /auth/*, /cal/*) reach the Worker; everything else is served as a static asset.Tests. guild-accounts-worker-tests.js drives the account flows (Battle.net sign-up and sign-in, linking and unlinking Discord, the state of each flow, the last-identity and cross-account rules, membership through a roster link, the officers' account link and look-up, Delete my data, the display name) and migration 0012 on a populated database, and guild-accounts-ui-tests.js covers the sign-in choice, the Linked accounts cards and the person page's account field. guild-bnet-worker-tests.js drives the Battle.net link, claims, settings, roster import and the 30-day purge over the same D1 fake with tools/fake-battlenet.js on a local port, and guild-bnet-ui-tests.js covers the Battle.net pages (the routes through the real router are in guild-shell-tests.js). guild-attendance-worker-tests.js drives the raid, attendance, policy and backfill routes over the same D1 fake (Raid-Helper and Warcraft Logs faked in process). guild-loot-worker-tests.js does the same for the loot history (Gargul import, tags, balance, the views, visibility and the loot policy), and guild-loot-contract-tests.js runs the real pages against that Worker so a field one side names differently shows up as a blank cell. guild-signups-worker-tests.js covers native sign-ups and the schedule (windows, locks, permissions, the merge with Raid-Helper rows, the planner seed, DST-safe filling) and holds a golden proving the attendance of older raids did not change, guild-signups-ui-tests.js the panel, the Raids list and the schedule page, and guild-signups-contract-tests.js runs those pages against that Worker (and the Worker's planner seed through the Planner's own reader). guild-wave1-worker-tests.js covers the officer tools above (the pure halves: sentences, CSV, ICS escaping and folding, the watch-list maths, the attention builder; then every route over the D1 fake, including the D1 call budgets), guild-wave1-ui-tests.js the pages, and guild-wave1-contract-tests.js runs those pages against that Worker (and fetches a real feed). guild-worker-tests.js runs the Worker over tests/d1-fake.js (a node:sqlite stand-in for D1 that runs the real migration and copies D1's limits), guild-roadmap-tests.js and guild-roadmap-worker-tests.js cover the roadmap page, its data file, the idea box routes and the triage tool, guild-config-tests.js pins the config files, and guild-ui-tests.js, guild-roster-ui-tests.js and guild-shell-tests.js cover the shell (the last drives it against fake documents).
User-visible changes get an entry at the top of CHANGELOG in js/changelog.js and a matching bump of APP_VERSION there and version in package.json (a test checks they agree). whats-new.html lists every release; the header's What's new link shows a "New" badge until someone opens that page, once per version (the stored version is under pp_whats_new_dismissed_v1).
worker/share.js | Short and live share links (/api/share, /tbc/:id, /classic/:id, /forever/:id) on the D1 table share_links (migration 0028): 30-day expiry swept by the daily cron, per-IP rate limit in rate_limits. |
scenarios.js | Test rosters. Also fetched by the ?dev roster picker (open /?dev). |
tools/ | rules-report.js (prints a version's weights and tuning numbers for review) and generate-rules-data.js (a guarded one-shot record that refuses to run). Not deployed. |
icons/ | Self-hosted spec, class, role and buff icons. |
tests/ | The test suites (tests/planner/, tests/guild/, tests/site/), their runner and helpers. Not deployed. |
exclusiveBuffs | Ids that share one slot per party with this one (none in Forever since 2026-10-16); the better-valued member wins. | Buff ids of the same version; both members list each other. |
utilities |
| The class-presence checklist (battle rez, cleanses, ...). |
Id -> { name, providers: [{ class, spec?, note? }], desc }. |
readiness: truetotalitemIdlatestMissedRaidIdreviewedGET /api/guild/:gid/roster/:pid/trialPOST /api/guild/:gid/calendar/cal/<token>.icsDELETEGET /cal/<token>.icstext/calendarwrangler.jsoncrun_worker_first/cal/*npm run guild:migrate0013/#/guest/<token>__Host-rs_guestGET /api/guestworker/viewer.js/#/public/<guild id> (GET /api/public/:gid) for anyone, and the built-in Public role is the ceiling. Never in a guest or public view: officer notes, Discord names or IDs, nicknames, accounts, sign-up details, attendance reasons, loot notes. Anonymous reads are not rate limited by the Worker (no IP is stored); put a Cloudflare rate-limit rule on /api/public/*, /api/guest and /auth/* at the platform move.node tools/fake-discord.js --bot-servers all --roles '{"<serverId>":[{"id":"1","name":"Officers","position":5}]}' --members '{"<serverId>":{"<userId>":["1"]}}' answers the two bot calls (token fake-bot-token, or --bot-token); /__fake/roles, /__fake/member, /__fake/bot and /__fake/state change it at runtime, and opening the invite link adds the bot. Point the Worker at it with --var DISCORD_API_BASE:http://127.0.0.1:8790 --var DISCORD_BOT_TOKEN:fake-bot-token.guild-roles-worker-tests.js (permission resolution with custom, Discord-given and manual roles, migration 0014 also on a populated database, the role editor and its guards, the mapping, sync and fail-safe paths, guest links, the public page and the exact fields of the read-only view, cross-guild isolation of every new route), guild-roles-ui-tests.js (the pages) and guild-roles-contract-tests.js (the real pages against the real Worker).
Sign-up lifecycle and raid sizes (PM wave 2, migration 0015; 0014 is the roles branch). Eight features on the existing Worker, no new variables or fakes; the design and contracts are at the bottom of tasks/reference/guild-pm-wave2-spec.md. (1) Raid sizes - a team's size is editable (10 / 20 / 25 / 40, Guild settings > Teams), a raid's size is its own override, else its zone's size (RAID_ZONE_SIZES in worker/raid-zones.js, pinned against js/rules/*.js), else the team's; counts read "18 / 20", Needs attention compares against it, and a guild raid opens in the Planner at that size (seed.raid.size / zone, RaidLink.raidKeyFor: a Forever Hyjal Summit raid opens at 20). PATCH /api/guild/:gid/raids/:rid { size, signupCloseHours } (raids.edit, null clears). (2) Sign-up deadline - Raid schedule settings "Close sign-ups N hours before the start" (0 to 72, teams.signup_close_hours) and a per-raid override: a member's window closes at start minus N hours (409 signups_closed, reason: 'deadline', closesAt); officers are never bound. The arithmetic lives in worker/signup-window.js and again in WINDOW_OPEN_SQL inside every member write. (3) Date-only raids - raids.time_known (0 = only a date, the start is noon UTC): the raid page says "Time not set" and the calendar feed emits an all-day event. (4) Away notices - #/g/:gid/away (avatar menu, Raids page): Away, Late or Leaving early, a date range or every weekday, a reason only the owner and officers see; routes /api/guild/:gid/away/mine, /away, /away/:nid. An Away notice is a virtual, weakest Absence on upcoming raids (any real sign-up wins) and, when it was filed before the raid began, turns a missed raid into "missed with notice" (worker/away.js; loadDay / consolidateDay in worker/attendance-store.js); without notices every attendance result is byte-identical (the golden in guild-wave2-worker-tests.js, recorded on the unchanged base). (5) Planner warnings - the seed (GET /raids/:rid/planner) marks people who are Away, Also in another raid of the guild that starts within 4 hours, Late / Leaving and Standby (signups[].marker { kind, text }, ASCII); the Planner shows a tag, files "Also in" players like Bench sign-ups (Optimize never seats them, an officer's drag does) and shows no start time for date-only raids. (6) Volunteer standby - a sign-up kind stored as signed + raid_signups.standby = 1 (so the kind CHECK is untouched; readers derive the effective kind), counted as signed up, in its own group on the panel; attendance ranks it between bench and tentative, so a plan bench makes it a bench credit; in the Planner, Optimize holds Standby players out of the first pass when there are more candidates than seats (Optimizer.holdStandby; role floors win, and the existing value trade can still pull one back in). (7) Standing sign-ups - "I'm in for every Thursday" (GET /standing, PUT /teams/:tid/standing): reconcileStanding in worker/standing.js creates the member's sign-up (raid_signups.standing = 1) on upcoming raids of that weekday when they have no sign-up and no Away notice covers it, after saving, schedule fill, creating a raid, an Away change and a Raid-Helper link; any edit makes the row the member's own, and removing one turns it into their Absence so it is not made again. (8) Publish the roster - POST /raids/:rid/roster snapshots the plan into raid_roster; members see their group (GET .../roster gives each only their own seat) and Confirm or Can't make it (the sign-up becomes Absence, the raid shows "N rostered people can't make it" in Needs attention); re-publishing keeps the confirmations of seats that did not change. Run npm run guild:migrate once after pulling migration 0015. The suites are guild-wave2-worker-tests.js (golden first), guild-wave2-ui-tests.js, guild-wave2-contract-tests.js and, for the Planner side, wave2-planner-tests.js and raidcontext-tests.js.GET /__fake/webhook-postsGET /__fake/webhook-mode?status=404node tools/guild-smoke.js run --name <x> tools/guild-smoke-notify.jsnpm run guild:migrate0018guild-wave4-worker-tests.jsguild-wave4-ui-tests.jsguild-wave4-contract-tests.jsattendance.reviewPUT /roster/:pid/rankperson.rankPOST .../rank/dismissnpm run guild:migrate0021raids.guest_limit0041POST /api/join/viewPOST /api/joinrun_worker_firstguild-isolation-tests.jsworker/roster-paste.jsraid_manual_names00220023notification_lograid_loot_listsnpm run guild:migrateguild-wave8-worker-tests.jsguild-wave8-ui-tests.jswave8-planner-tests.js