Changelog

Notable updates to RealmForge, newest first.

minorcleanupsecuritymaintenanceJun 30, 2026

Minor bug fixes

minorsecurityinfrastructureMay 22, 2026

Minor bug fixes

minorsecurityinfrastructureMay 21, 2026

Minor bug fixes

minorcardsability-dslparserengineMay 8, 2026

Minor bug fixes

majoraspectsphase-rsenginemodalbugfixMay 7, 2026

Aspects engine: modal spell branches now actually resolve

Bug: Master Vendrek's Demonstration's mode 1 (deal 1 damage to each opp unit) worked, but mode 2 (deal 4 damage to target unit) gave the choice prompt and then did nothing — the chosen branch's DealDamage effect silently resolved with an empty target list. By extension, any future modal spell with a target-requiring branch would fail the same way.

Root cause: the phase-rs translator was emitting Effect::ChooseOneOf with a branches field. The engine HAS that variant, but collect_target_slots (which walks an ability's effect tree at cast time to collect target choices the player must make) does NOT walk into the branches array. So mode 2's target slot was never created, never picked, and at resolution time the DealDamage effect had nothing to damage.

The engine's canonical modal-spell shape is different. Per crates/engine/src/database/forge/translate.rs:171: outer ability has effect=Unimplemented{name:'modal_placeholder'} as a sentinel, plus modal: ModalChoice { min_choices, max_choices, mode_count, mode_descriptions, ... } and mode_abilities: AbilityDefinition[]. collect_target_slots WALKS mode_abilities and collects per-mode target slots up-front, so each mode's branch effect gets exactly the targets it needs at resolution.

Fix in scripts/ability_translator.cjs's tryModalChooseOne: replace the ChooseOneOf emission with the modal-placeholder + ModalChoice + mode_abilities shape. Verified via curl that Master Vendrek's now serializes correctly: outer effect type 'Unimplemented', modal.mode_count=2, mode_abilities[0] is DamageAll, mode_abilities[1] is DealDamage. New card-data.json hash deployed.

If you still see multi-ability cards drop their second ability, that's likely a different category of bug — flag specifics and I'll investigate.

patchcardsui-polishcard-frameMay 7, 2026

Card frame: world-name region auto-fits longer names without trailing into the logo

Long world names like ALDENCROFT were trailing into the ASPECTS REALMFORGE logo at the bottom of the card frame because the auto-fit logic was too optimistic about glyph width with tracking-widest (0.1em letter-spacing).

Three small tweaks in CardPreview.tsx, scoped to the worldRect region only:
• Padding 0 3%0 1% (recovers ~4% of usable width, mostly on the left).
• Glyph-width estimate bumped from 0.55 + 0.1 = 0.65 to 0.62 + 0.1 = 0.72 em-per-char so the loop picks a smaller font when needed.
• Floor lowered 6px → 5px so very long names still squeeze in instead of bleeding past the region.

Oracle and Flavor regions kept their original 3% padding — only the world-name region was tightened.

patchcardsability-dslparserMay 7, 2026

Parser: {T}: tap-for-mana now works on any card type, not just ASPECTs

patternTapForMana had if (cardType !== "ASPECT") return null; at the top, rejecting {T}: {A}{A} shorthand on UNIT cards (and FIELD/ACTION/ANSWER/etc.). Activated mana abilities are valid on any card type — think MTG mana-dorks like Llanowar Elves. Gate removed. The handler's regexes only match sentences that LITERALLY start with {T}:, so non-mana sentences fall through cleanly without being mis-claimed.

patchcardsproposeability-dslui-polishMay 7, 2026

Propose form: tap shorthand, world/year on preview, schema-edit link

Three follow-ups on /cards/propose after the polish pass:

  1. ABILITY PARSER — {T}: {A}{A} shorthand. patternTapForMana now accepts the no-Add-verb form and groups repeated symbols. {T}: {A} → addAspectToPool A:1. {T}: {A}{A}{A} → addAspectToPool A:3. Multi-letter {T}: {A}{B} emits one ability per letter. Existing {T}: Add {X} and {T}: Add {X} or {Y} patterns unaffected. (Was: parser flagged 'No ability patterns matched' for the shorthand even though it's the conventional way authors write basic-land tap costs.)
  1. LIVE PREVIEW — world name + year. The preview's worldRect/yearRect regions were rendering blank because the propose form's previewCard object wasn't passing worldName or createdAt. Added both: worldName comes from the selected world; createdAt is set to new Date() for preview-time (server stamps the real value at submit). Card frames now read 'AELINDRA · 2026' (or whatever world+year applies).
  1. SCHEMA-EDIT LINK — added next to the aspect-cost slot counter. Goes to /worlds/<slug>/cards/schema (the per-world aspect schema editor — labels, colors, icon URLs). Renders only when a world is selected. Note: the schema page is universally available — every world auto-upserts a CardSchema row on first visit. The earlier transient 404 on /worlds/aelindra/cards/schema was a side-effect of the .next directory race we hit this morning, not a missing route.

Files: src/lib/cards/abilityParser.ts (shorthand branch), src/components/cards/CardProposeForm.tsx (preview fields + Link import + schema-edit anchor).

minorcardsproposeuipolishMay 7, 2026

Minor bug fixes

minorworldsuicustomizationMay 7, 2026

Minor bug fixes

patchworldsui-polishMay 7, 2026

World page: Magic Systems moved under Tech Eras

Section reorder on /worlds/[slug]: Magic Systems now appears immediately after Tech Eras, before Cyclical Events. Pure block move; nothing else changed. (User asked to either move the section or make all sections drag-reorderable; the latter is queued as a follow-up since it needs a schema field, a drag library, and an edit-mode toggle.)

patchworldstechui-bugfixMay 7, 2026

Fix: world Tech Level falls back to most recent tech era

World detail page (Technology stat row) was reading planet.techLevel directly. If the world owner never explicitly set a top-level Tech Level on the world but DID add tech eras with their own techLevel values, the row showed 'Unknown' even though the world clearly has a defined tech state.

Fix: when planet.techLevel is null and the world has at least one tech era, the page now derives the displayed tech level from the most recent era (highest startYear, BigInt-aware sort; falls back to the first era when none have a startYear set). The world's explicit techLevel still wins when it's set.

Touched: src/app/worlds/[slug]/page.tsx (line ~178 — added eraTechLevelFallback derivation, no other behavior changes).

minorworldbuildingtech-erastimelinebugfixMay 7, 2026

Minor bug fixes

majoraspectsability-dslparserengineMay 7, 2026

Aspects DSL parser — 24-card coverage push

Context: the in-house Aspects v2 ability DSL parser (src/lib/cards/abilityParser.ts) was leaving every one of the 24 approved Aspects-set cards in the admin Ability Queue marked unresolved (0 clean / 1 partial / 23 unresolved at start). Aspects v2 engine cards need clean DSL to mechanically execute; partial parses still let the UI show what was authored.

Added 12 new keyword EffectKinds to abilityDSL.ts so bare-keyword lines compile to typed Abilities: trample, menace, haste, firstStrike, doubleStrike, prowess, lifelink, reach, defender, indestructible, hexproof, deathtouch. Also added two continuous global-prohibition kinds (noLifeGain, damageCantBePrevented) and three structural kinds (bouncePermanent for the broader return-to-hand target shape, discardThenDraw for Tersha's ETB, exileFromDiscard for Pale-Sister/Liturgy field abilities).

Wired matching cases into abilityResolver.ts so each new EffectKind logs a tagged keyword:* entry and degrades gracefully — no engine crashes if persisted DSL references a kind the engine helpers haven't fully implemented yet.

Added pattern handlers in abilityParser.ts:
• patternKeywordList — single bare keyword line + comma-separated lists (Flight, haste).
• patternNoLifeGain / patternDamageCantBePrevented — global prohibition continuous abilities.
• patternConditionalFirstStrike — "This unit has first strike during your turn" (the during-your-turn qualifier is dropped at the DSL level).
• patternTapForMana — basic and multi-mode {T}: Add {X}. and {T}: Add {X} or {Y}. with reminder-paren tolerance.
• patternEntersTappedUnless — aspect-side ETB-tapped condition.
• patternNamedDealDamage / patternDealDamageEachOppUnit / patternDealDamageAnyTarget / patternIfKickedDamage — covers Cinder-Tap, Pulse-Lance Strike, Veil-Lash Pulse base + kicker, and Master Vendrek's Demonstration's two modes.
• patternBounceTarget — Thal Hush-Rite's return-to-hand.
• patternEtbDamageOppUnit — Auroral Skywyrm's ETB damage line.
• patternOpponentCastsCheapDamage — Geyser-Coil Sentinel's spell-cast trigger.
• patternOpponentDrawsTrigger — Stitched-Smile's draw trigger (continuous noOp stub until onOpponentDraw lands).
• patternDiscardThenDraw — Tersha's ETB.
• patternEtbExileFromDiscard / patternExileEachOpponentDiscard — Pale-Sister Lantern's ETB and tap-and-sac.
• patternAlternateCostKickerCurly — Kicker {F}{F}{F}{F} cost form (in addition to the existing numeric Kicker N).
• patternActivatedBuffSelf / patternActivatedAddMarkSelf / patternActivatedCreateToken / patternActivatedGeneric — activated-ability wrappers that strip the curly-brace cost prefix and delegate to parseInlineTail.

Other changes: splitSentences now rejoins "Choose one —" header lines with their bullet-led mode lines into a single "Choose one — <A> or <B>" sentence so patternChooseMode matches without modification. patternAddAspect's "to your Pool" suffix is now optional so it can match as the LHS of an "X, then Y" composition (Liturgy of the Kindler).

Coverage result on the 24 approved Aspects cards (after re-stamping abilitiesDSL):
• 10 CLEAN: Geyser-Coil Sentinel, Stitched-Smile Cinderkin, Pale-Sister Lantern, Master Vendrek's Demonstration, Thal Hush-Rite, Cinder-Tap, Pulse-Lance Strike, Veil-Lash Pulse, Singing Range Outcrop, Spire-Cliff Tide-Channel.
• 13 PARTIAL: Auroral Skywyrm (Warp skipped), Emberheart Caerit Champion (Valiant skipped), Pyre-Tendon Vrekkar (Start your engines! / Max speed skipped), Sandsearer Mercenary (attack-with-Drekarin trigger skipped), Solven, the Kindler (Pyre-channeling skipped), Solvinar-Spine Caerit (count-driven ETB damage skipped), Tersha Chordbreaker (attack trigger skipped), Vendal, Stone-Captain (damage modifier skipped), Liturgy of the Kindler (exile-top-of-deck skipped), Cliffroost Hamlet (multi-subtype buff activated skipped), Pillar of the Wandering Stone (becomes-a-unit skipped), Slow-Chord Pillar (next-time damage doubling skipped), Tideglass-Pyre Verge (activate-only-if rider partial-stripped).
• 1 UNRESOLVED: Chordbane Drekarin (damage equal to count of cast non-units — too dynamic for a static dealDamage param).

Skipped clauses are documented in the parser comments. Partial parses still produce typed Abilities for the clauses that did parse — the engine fires those, the rest survive on rawText.

Validation: 69/69 self-test cases pass (was 48/48; 21 new test entries cover the new patterns). Typecheck clean. Build deployed via safe_build_restart.sh — no manifest race.

majoraspectsphase-rstranslatorenginecoverageMay 7, 2026

Aspects oracle-text translator (second pass) — full card-by-card coverage

Systematic pass over every non-aspect card. Goal: "as complete as phase-rs can express." 5 new patterns added on top of the first-pass translator. Every untranslatable mechanic documented against engine source.

Coverage delta vs first pass:
• Stitched-Smile Cinderkin: conditional first strike during your turn — added as StaticDefinition Continuous with AddKeyword + DuringYourTurn condition. (Templated from rancor/bonesplitter snapshots; StaticCondition::DuringYourTurn defined at types/ability.rs:2728.)
• Pyre-Tendon Vrekkar: "{2}: This unit gets +1/+0 until end of turn." — activated Pump with AbilityCost::Mana. (Templated from bonecrusher_giant; Effect::Pump at types/ability.rs:3480.)
• Sandsearer Mercenary: "{1}{R}: Put a +1/+1 mark on this unit." — activated PutCounter (P1P1). Activation gating ("if opp lost life this turn / once each turn") skipped — RequiresCondition shape isn't documented in any snapshot for our specific predicates.
• Auroral Skywyrm: "When this unit enters, it deals 1 damage to target unit an opponent controls." — ChangesZone ETB trigger with valid_card SelfRef + destination Battlefield, execute=DealDamage to Typed{Creature, controller=Opponent}. (Templated from goblin_chainwhirler snapshot.)
• Solvinar-Spine Caerit: "Players cannot gain life." — StaticMode::CantGainLife unit variant (defined types/statics.rs:378), serializes as bare string. Wired through dump_aspects_cards.cjs line 474 by adding static_abilities field to the export entry.

Final coverage matrix (18 non-aspect cards):
• ✓ Fully translated (12): Cinder-Tap, Pulse-Lance Strike, Veil-Lash Pulse (base), Thal Hush-Rite, Master Vendrek's Demonstration (modal), Stitched-Smile (FS-during-turn + opp-draws trigger), Auroral Skywyrm (Flying/Haste + ETB damage), Vendal Stone-Captain (Trample), Pyre-Tendon Vrekkar (Menace + activated Pump), Tersha Chordbreaker (Haste), Emberheart Caerit Champion (Haste/Prowess), Geyser-Coil Sentinel (opp-casts-cheap trigger).
• ⚠ Partial (1): Sandsearer Mercenary — activated +1/+1 baked, attack-trigger and activation gating skipped.
• ✗ Skipped with documented reason (5): Solvinar-Spine ETB damage (data-model conflict — every aspect is Basic for mana to work, counting non-basics = 0); Solven (Pyre-channeling custom + {F}×8 cost is unpayable, ManaColor is WUBRG only); Pale-Sister Lantern (discard-pile semantics + {F} costs); Liturgy of the Kindler (aspect tokens custom + {F} costs); Chordbane Drekarin ("damage equal to non-units cast this turn" — QuantityRef shape uncertain in this context).

Untranslatable mechanics (all need Aspects v2's clean-room engine):
• Warp, Pyre-channeling, Valiant, Start Your Engines / Max-speed, aspect tokens — no engine primitive.
• {F} mana symbol — phase-rs's ManaColor enum is WUBRG only (types/mana.rs:9). Any {F} shard in a Mana cost fails serde deserialize and would break DB load. Affects Solven's 8-F token cost, Pale-Sister/Liturgy F-tap-sac, Veil-Lash kicker.
• "non-basic aspects" semantic — current data model marks every aspect Basic so synthesize_basic_land_mana auto-attaches the tap ability. Until we have a non-Basic aspect category, Solvinar-Spine's count is 0.
• Continuous "damage cannot be prevented" static — engine only models this as one-shot Effect::AddRestriction (per questing_beast); no StaticMode variant.
• Subtype-source damage amplifier (Vendal's "+1 dmg from Cinder sources") — replacement-static shape with subtype filter, no comparable snapshot.
• Activation gating like "only if opp lost life this turn / once each turn" — RequiresCondition exists but ParsedCondition shape isn't documented for these specific predicates.
• discard-pile-as-graveyard semantic equivalence — risky to encode without verifying our data model treats them identically.

Verification: curl-fetched served card-data.json post-redeploy. 24 cards loaded (no card lost). Pre-v2 cards (Cinder-Tap, Pulse-Lance, Vendrek's modal, Thal Hush-Rite, Geyser-Coil, Veil-Lash, keyword-only cards) byte-stable. New patterns emit fields whose shapes byte-match canonical engine snapshots. New card-data hash: originis-802401c8b002.

Files touched:
• scripts/ability_translator.cjs — 5 new tryFooBar patterns + static_abilities returned in public API.
• scripts/dump_aspects_cards.cjs (line 474) — wired translator.static_abilities into export entry.
• public/aspects-app/{card-data,aspects-cards,card-data-meta}.json — refreshed via re-dump + mirror.

Headline takeaway: phase-rs is now expressing as much of the Aspects ruleset as its enums physically support. The remaining gap is the v2 engine's job — that's exactly what the 2026-05-06 clean-room rebuild is for.

majoraspectsphase-rschromebrandingmultiplayerMay 7, 2026

RealmForge chrome rebrand for the Aspects SPA

End-to-end chrome rebrand of the /aspects-app/ SPA so it presents as RealmForge, not phase.rs.

Branding:
• Browser tab title: "RealmForge — Aspects" (was "phase.rs").
• PWA manifest name/short_name: "RealmForge — Aspects" / "RealmForge". start_url corrected to /aspects-app/. Manifest icons re-pointed to /aspects-app/icons/.
• Hero + splash logo: full RealmForge logo (circular sigil over REALMFORGE wordmark, 1605×751) replaces the phase.rs cards-and-gear graphic. Sourced from the canonical brand asset.
• Favicon set rebuilt from the square sigil — favicon-16x16, favicon-32x32, favicon.ico (32×32 PNG bytes), apple-touch-icon (180), icon-192, icon-512. Resized via System.Drawing locally because the server has no ImageMagick/cwebp.

Nav links (top-left of MenuPage):
• Removed: GitHub link and Sponsor link (entire <a> blocks plus their now-unused SVG icon components).
• Added: Home link (/realmforge.net/), placed BEFORE Discord, with house icon, full-page nav (no target=_blank), aria-label "Home — return to RealmForge".
• Discord URL: https://discord.gg/JeSvDHP5qU (was discord.gg/dUZwhYHUyk). Defined in DiscordBadge.tsx, not inline.
• Ko-fi URL: https://ko-fi.com/cyberfortress (was ko-fi.com/phasers).
• Final order: Home → Discord → Ko-fi.

Multiplayer (Play Online → lobby):
• Direct-connection / dedicated-server mode is now HIDDEN. Server-mode UI no longer renders.
• P2P is always selected; the implicit mode-switch effect (flipping to server when serverAddress was set) is disabled.
• "Pick server" chip removed from the lobby chrome — no escape hatch back to server mode.
• Underlying services (openPhaseSocket, multiplayerSession, ServerPicker) NOT deleted — just unreferenced from UI. Easy to bring back when the upstream phase-server becomes Aspects-aware.
• Why: only functional decks are custom Aspects cards; phase-server has no knowledge of these, so direct-connect mode would be unplayable.

Files touched:
• vendor/phase-rs/client/index.html (title)
• vendor/phase-rs/client/public/manifest.json (rewritten)
• vendor/phase-rs/client/public/{logo.png, favicon-16x16.png, favicon-32x32.png, favicon.ico}
• vendor/phase-rs/client/public/icons/{apple-touch-icon,icon-192,icon-512}.png
• vendor/phase-rs/client/src/pages/MenuPage.tsx (nav links + new HomeIcon)
• vendor/phase-rs/client/src/components/chrome/DiscordBadge.tsx (Discord URL constant)
• vendor/phase-rs/client/src/components/menu/MenuLogo.tsx (logo src)
• vendor/phase-rs/client/src/components/splash/SplashScreen.tsx (logo src)
• vendor/phase-rs/client/src/pages/MultiplayerPage.tsx (forced p2p mode)
• vendor/phase-rs/client/src/components/lobby/LobbyView.tsx (Pick-server chip removed, info note rewritten)

Known holdovers (deliberately left for user decision):
• Page-footer MIT attribution at MenuShell.tsx:71 still reads "Aspects — built on phase-rs (MIT). github.com/phase-rs/phase" — left intact as a license credit, not a nav-style GitHub link.
• Hero halo geometry is square (256px); the wordmark portion of the full logo will sit centered with whitespace above/below. Visual decision deferred to user.

Deployed via safe_phase_rs_redeploy.sh. Verified: served logo.png is 1605×751, served manifest.json reads RealmForge, all favicon paths return 200, https://realmforge.net/aspects-app/ tab title reads "RealmForge — Aspects".

majoraspectsphase-rsenginetranslatorbugfixMay 7, 2026

Aspects oracle-text translator (first pass)

Background: phase-rs's WASM card-database load path doesn't run synthesis or oracle-text parsing. Every non-aspect card was loading vanilla — name + stats only — so spells fizzled, units had no keywords, and triggered abilities never fired.

Built a conservative oracle-text → engine-shape translator (scripts/ability_translator.cjs) wired into dump_aspects_cards.cjs. Translates the patterns we can match safely and skips anything ambiguous rather than fabricating wrong rules.

What it now translates:
• Standalone keywords on units: Flying (incl. our "Flight"), Haste, Trample, Menace, First Strike, Double Strike, Vigilance, Lifelink, Reach, Defender, Indestructible, Hexproof, Prowess, Deathtouch, Flash.
• Direct-damage spells matching "<Name> deals N damage to any target": Cinder-Tap (2), Pulse-Lance Strike (3), Veil-Lash Pulse (2 base — kicker upgrade NOT translated).
• Bounce: "Return target non-aspect permanent to its owner's hand" (Thal Hush-Rite) — encoded as Effect::Bounce with TargetFilter::Typed{[Permanent, Non(Land)]}.

Currently NOT translated (left as vanilla until Aspects v2 engine ships, since machine-translating these wrong is worse than translating them not at all):
• Triggered abilities: Stitched-Smile (opp draws), Sandsearer (attack), Geyser-Coil (opp casts cheap), Tersha (ETB + attack), Auroral Skywyrm (ETB damage).
• Modal spells: Master Vendrek's Demonstration ("Choose one —").
• Kicker / alt costs: Veil-Lash Pulse upgrade, Auroral's Warp.
• Self-counting damage: Solvinar-Spine, Chordbane Drekarin.
• Custom Aspects mechanics: Pyre-channeling (Solven), Start Your Engines (Pyre-Tendon), Valiant (Emberheart), aspect tokens, F mana costs as activation costs.
• Activated abilities with sac cost: Pale-Sister Lantern, Liturgy of the Kindler, Solven token-creation.
• Static prohibitions: Solvinar-Spine "Players cannot gain life" / "Damage cannot be prevented", Stitched-Smile conditional first strike.

Net result: 9 of 18 non-aspect cards now have at least partial engine support (4 spells + 5 unit keyword sets), up from 0. The remaining 9 still cast and deal combat damage normally, but their text-driven rules don't fire. Hard-refresh the SPA + start a new game to pick up the changes.

majoraspectsphase-rsenginebugfixMay 7, 2026

Aspects produce mana in-game again

Diagnosed via screenshot: aspects on the field but no mana available to cast spells. Three layered bugs:

1) The phase-rs engine reads /aspects-app/card-data.json (NOT aspects-cards.json or scryfall-data.json), but card-data.json was stale — frozen on 2026-05-06, missing all 6 saga/transform replacement cards and still containing the 6 cards we replaced.

2) The WASM card-database load path (CardDatabase::from_json_str → from_export_entries) does NOT run synthesize_all. Synthesis only runs on the MTGJSON load path. So even though aspects had Basic Land — Mountain typing in the JSON, the engine never auto-attached the {T}: Add {R} ability — every aspect ended up vanilla with empty abilities.

3) Cliffroost Hamlet was being typed as Wastes (engine has no ManaColor::F variant), even though the card also produces {D} (Mountain).

Fixes:
• dump_aspects_cards.cjs: detection now scans EVERY {T}: Add {X} line + name suffix + legacy prose at once, prefers non-F so dual-producers like Cliffroost get Mountain.
• dump_aspects_cards.cjs: for ASPECT cards, pre-bakes the activated {T}: Add {color} mana ability into the abilities array (matching the runtime_card_export_fixture.json shape) so the engine sees the ability the moment it loads card-data.json.
• safe_phase_rs_redeploy.sh: mirrors aspects-cards.json onto public/aspects-app/card-data.json on every redeploy and updates the meta hash. The previous version had this copy and a 2026-05-06 rewrite silently dropped it — that is what caused card-data.json to drift.

All 6 aspects (Singing Range Outcrop, Cliffroost Hamlet, Tideglass-Pyre Verge, Spire-Cliff Tide-Channel, Slow-Chord Pillar, Pillar of the Wandering Stone) now serve with subtype Mountain or Island and a baked tap-for-mana ability.

majoraspectsenginecutoverphase-10internalMay 6, 2026

Aspects v2 — phase-rs ripped out, in-house engine live at /cards/play

Aspects v2 — Phase 10 cutover (in-house engine replaces phase-rs)

The /cards/play page now renders the in-house Aspects engine directly. No more iframe, no more vendored Rust+WASM SPA, no more phasersoff plugin layer.

What was removed

  • vendor/phase-rs/ (1.9 GB) — entire upstream phase.rs clone, source + WASM + dist
  • public/aspects-app/ (422 MB) — built phase-rs SPA assets
  • src/app/cards/play/solo/play/ — old SoloPlaySurface iframe wrapper
  • src/app/cards/play/aspects-v2/ — temp scaffolding folder (the UI lives at /cards/play directly now)
  • src/app/api/admin/plugin/{export,import,save,test-card-source,uninstall,upload}/ — phasersoff admin routes
  • src/app/api/plugins/[...path]/route.ts — file-system plugin streamer
  • src/app/api/aspects/card-data/route.ts — vanilla MTG ↔ phasersoff card-data merge route
  • All /aspects-app/*, /audio/*, /feeds/*, /battlefield/*, /preview-icons/*, /logo.*, /scryfall-data.json, /card-data*.json, /card-names.json, /coverage-*.json, /decks.json, /set-list.json, /assets/*, /icons/*, /favicon-*, /aspects-cards.json, /aspects-schema.json, /aspects-card-frames.json rewrites in next.config.ts

What's at /cards/play now

  • src/app/cards/play/page.tsx — auth gate + renders <Aspects />
  • src/app/cards/play/Aspects.tsx — client component (~17KB). Renders opponent + stack + your battlefield + hand. Click cards in hand to cast / play; click ASPECT permanents to tap for mana; buttons for pass priority / declare attackers / declare blockers / concede.
  • Auto-passes priority through non-decision phases so the human only stops when there's actually a choice.

Verification (all green)

  • GET /cards/play → 307 to /login when unauth (auth gate works)
  • GET /aspects-app/index.html → 404 (gone)
  • GET /card-data.json → 404 (gone)
  • GET /api/plugins/phasersoff/manifest.json → 404 (gone)
  • POST /api/aspects/match/solo → 401 unauth (engine API still up)
  • safe_build_restart.sh ran clean; service active.

Known limitations to fix in follow-ups

  • Card definitions fetched via GET /api/cards/[id] per id; bulk fetch would be cleaner
  • No card-art on the board yet (just text chips); plug into your existing Card.artUrl next
  • Activated/triggered abilities aren't wired (the engine handles them, but the existing 24 cards' abilities live in free-form text — needs ability-DSL parsing in Phase 5)
  • No multiplayer; solo-vs-AI only. Phase 8b (WebSocket PvP) deferred
  • No replay viewer UI yet (logs are persisted in AspectsSoloMatch.log)
  • No mulligan flow (always keep your opening 7); cards skip the mulligan-decision step

Disk freed

  • ~2.3 GB on the prod box

Engine ready to play. Open https://your-host/cards/play, log in, click "Start a match".

minoraspectsenginephase-8ainternalapiMay 6, 2026

Minor bug fixes

minoraspectsenginephase-7internalcronMay 6, 2026

Minor bug fixes

minoraspectsenginephase-4internalMay 6, 2026

Minor bug fixes

minoraspectsenginephase-3internalMay 6, 2026

Minor bug fixes

minoraspectsenginephase-2internalMay 6, 2026

Minor bug fixes

minoraspectsenginephase-1internalMay 6, 2026

Minor bug fixes

minoraspectsenginephase-0internalMay 6, 2026

Minor bug fixes

minorpluginphasersofffeaturevttMay 6, 2026

Minor bug fixes

majorcardsenginecontentMay 5, 2026

Cards: Adventure layout + cost cap bump + first deck-conversion run

Adventure layout joined Transform and MDFC: CardLayout enum is now SINGLE | TRANSFORM | MDFC | ADVENTURE; dumper emits layout: adventure on the JSON pair so phase-rs CardLayout::Adventure picks them up. AspectCost cap raised from 3 to 7 slots in both POST and PATCH zod schemas to allow CMC 4-7 cards (Ojer Axonil, Nova Hellkite, etc.). First Originis deck conversion landed: 21 unique MTG cards translated to Originis voice via locked subtype mapping (Lizard - Drekarin, Goblin - Stoneborn, Human - Valari/Dren, God - Resonance-Embodied, Dragon - Skywyrm, etc.) and inserted as 24 Card rows including 2 transform pairs (Vendal of the Slow-Time Stoneworks; The Saga of Solven the Kindler) and 1 adventure pair (Geyser-Coil Tideglass).

majorcardsengineschemaMay 5, 2026

Cards: transform / modal-DFC / Saga authoring support

InkFortress card model now supports two-faced cards end-to-end.\n\nSchema: new Card.layout enum (SINGLE | TRANSFORM | MDFC), Card.backFaceId self-relation, Card.faceGroupId shared between paired faces.\n\nPropose form: Layout dropdown + full Back-face panel (name, type, subtypes, aspect cost, power/endurance, abilities, flavor, art description, art upload).\n\nDumper: emits both faces as separate top-level entries in aspects-cards.json sharing one scryfall_oracle_id (= faceGroupId), tagged layout: transform / modal_dfc - phase-rs auto-pairs them into CardLayout::Transform via its oracle_id_index.\n\nSaga authoring: subtypes [Saga] on the front face + chapter-numbered ability text. Edit pages cross-link the two faces with a banner.

majorcardsaspectsresetMay 5, 2026

Originis card pool reset: 120 cards (2 decks) with movie-script art descriptions

Reset Originis Aspects card pool to 120 hand-authored cards arranged as 2 sealed decks (48 ASPECT, 36 UNIT, 16 ACTION, 8 ANSWER, 8 FIELD, 4 SIGNATURE). Every card now carries a screenwriter-style art_description paragraph (full WHO/WHAT/WHEN/WHERE/HOW/WHY blocking) feeding originis_grinder.py. Local SDXL Juggernaut grinder rendered all 120 PNGs to NAS in a 2.8h pass with 0 failures. Dump pushed to vendor/phase-rs/data/aspects-cards.json + public/aspects-app/ + public/card-data.json; service restarted.

minorcardsuxaspectsphase-rsMay 5, 2026

Minor bug fixes

patchaspectsphase-rsuxMay 5, 2026

ASPECT card text cleanup: drop duplicate ability line

ASPECT cards were rendering with the synthesized {T}: Add {F}. line AND the redundant DB-original Add 1 aspect token of Chord to your pool line concatenated together (e.g. "Hall of Chord" showed both). Dumper now skips card.abilities.text for card.type === "ASPECT", since the synthesized line already covers the same effect.

patchaspectsphase-rsdiagnosticsMay 5, 2026

Engine panic visibility from worker → main-thread console

WASM panic messages used to land only in the worker's own DevTools target, invisible to anyone debugging the main page. engine-worker.ts now drains take_last_panic_message() after every WASM call (and on every catch path) and posts a {type:"panic"} envelope to the main thread, where engine-worker-client.ts logs [ENGINE PANIC] <msg> via console.error. Cuts the observability gap that made the ETB-destroy STATE_LOST cascade hard to triage.

patchaspectsphase-rsstabilityMay 5, 2026

Render-resilience: defensive guards on .replace() callers

After parser-on-load went live, malformed parsed-ability shapes occasionally surfaced undefined string fields, which .replace() callers in the React render tree blew up on — unmounting the entire game tree mid-event ("destroy a blocker → crash → STATE_LOST"). Guarded ManaSymbol.tsx (renders ? chip on nil shard), keywordProps.ts:splitPascalCase, costLabel.ts default-branch fallback, and the prime culprit utils/description.ts:renderDescription (substitutes ~ self-reference; was throwing on undefined description from newly-structured triggers).

minorcardsframeMay 5, 2026

Minor bug fixes

minoraspectsphase-rsMay 5, 2026

Minor bug fixes

minoraspectsphase-rsabilitiesMay 5, 2026

Minor bug fixes

majorenginephase-rsaspectsMay 5, 2026

Engine: ManaColor::F first-class variant (replaces Colorless in Aspects)

Renamed ManaColor::ColorlessF across the engine, added BasicLandType::F variant with mana_color() returning ManaColor::F, and added ("Wastes", ManaColor::F) to synthesize_basic_land_mana. Updates the dumper, scryfall service, and SPA aspect schema accordingly. ManaColor is now WUBRGF (6 colors), not WUBRG + Colorless. Also fixed mana_payment.rs:50 [u32; 5][u32; 6] to match the new color count (was causing submitAction-panic index-out-of-bounds reports).

majoraspectsphase-rsengineoutageMay 5, 2026

Aspects basic-lands now produce mana (was a playability outage)

For most of the day, no card was castable on /cards/play after the F-aspect rename. Root cause was the SPA fetching the engine card-data from /card-data.json (default __CARD_DATA_URL__), but the dumper only wrote to /aspects-app/aspects-cards.json — engine read a stale snapshot pre-F-rename, treating lands as inert non-mana shells. Fix copies the freshly dumped JSON to /public/card-data.json AND updated safe_phase_rs_redeploy.sh to keep the two paths in sync. Also dropped the obsolete F-workaround in the dumper (F lands now carry Basic supertype like every other aspect, since the engine has BasicLandType::F + ("Wastes", ManaColor::F) in synthesize_basic_land_mana), and made CardDatabase::from_export_entries run synthesize_all on each face after deserialization.

patchuxnavsupportMay 5, 2026

Header: Ko-fi support link next to user dropdown

Added an inline-SVG coffee-cup button to the right of the user dropdown linking to https://ko-fi.com/cyberfortress in a new tab. Visible to logged-in users only (it lives inside the existing session ? branch).

minorcardsaspectsframespaMay 4, 2026

Minor bug fixes

patchdbmigrationoutagerecoveryMay 4, 2026

Recovery: changelog DB lost rows pre-2026-05-04 (data-loss schema push)

A prisma db push --accept-data-loss during the Wave 2 deploy on 2026-04-30 wiped existing ChangelogEntry rows. The site stayed up (build hadn't swapped yet), but the public timeline lost everything before 2026-05-04. This entry and the surrounding backfill restore the lost week from memory + deploy script timestamps.

majorcardsaspectsMay 3, 2026

Glyphs: in-platform currency + Stripe-backed glyph packs

New glyph wallet on every user (balance, transaction history) plus seeded glyph packs with Stripe price IDs. Card pack opens, vote rewards, and trade fees all settle in glyphs; packs purchasable through Stripe.

majorstoriesuxnavMay 3, 2026

Discovery rails: bookmarks, reads, favorites, follows, DMs

New social rails on the home and books pages: "continue reading" from book reads, "your bookmarks", "your favorites", "follow author" buttons on every book/profile, and inbound DM threading. Gives the discovery surface real Wattpad-tier engagement.

majorqueststabletopMay 3, 2026

Quest system: structured definitions + progress tracking

New Quest models cover quest definitions, objectives, rewards, and per-user progress tracking. Replaces the earlier ad-hoc LoreQuest blurbs with something the engine can actually progress, complete, and reward against.

minoruxnavMay 3, 2026

Minor bug fixes

majorcardsaspectsphase-rsengineMay 3, 2026

phase-rs Aspects engine LIVE: WASM, six colors, MTG slot remap

The Aspects card engine is now powered by phase-rs (Rust compiled to ~7.8MB WASM) vendored under /vendor/phase-rs/. The six aspect colors A-F map to MTG colors so the underlying rules engine can validate plays. /cards/play/solo and the SPA play surface are fully wired up.

minorauth2faMay 1, 2026

Minor bug fixes

minorstorieschapterswattpadMay 1, 2026

Minor bug fixes

minorstorieschaptersepubMay 1, 2026

Minor bug fixes

minorstorieschaptersuxMay 1, 2026

Minor bug fixes

minorstoriesnavMay 1, 2026

Minor bug fixes

minorstoriesepubApr 30, 2026

Minor bug fixes

minorstorieschaptersApr 30, 2026

Minor bug fixes

minorstoriesworldsApr 30, 2026

Minor bug fixes

minorstorieschaptersApr 30, 2026

Minor bug fixes

minorstorieschaptersApr 30, 2026

Minor bug fixes

minorworldsApr 28, 2026

Minor bug fixes

patchuximagegenworldsApr 28, 2026

Image upload UI on every authoring form

WorldArtifact, Fauna, Flora, and CharacterClass create/edit forms all gained an image upload control. Backed by /api/planets/[slug]/upload-image; affected POST/PATCH endpoints accept imageUrl. No more authoring without art.

minordndclassesgeartabletopApr 28, 2026

Minor bug fixes

minordndclassestabletopApr 28, 2026

Minor bug fixes

minornavuxtabletopcardsApr 26, 2026

Minor bug fixes

minorcardsaspectsimagegenApr 26, 2026

Minor bug fixes

minoruxnavApr 25, 2026

Minor bug fixes

minoruxnavworldsApr 25, 2026

Minor bug fixes

minorcardsaspectsApr 25, 2026

Minor bug fixes

minortabletopquestsApr 25, 2026

Minor bug fixes

majorstorieschaptersuxApr 25, 2026

Story extras: relationship graph, arcs, bookshelves, chapter comments

Stories now show a force-directed character relationship graph (walks chapter content for @-mentions). New StoryArc model groups chapters into arcs. Bookshelves let readers organise stories. ChapterComment (1-level threaded) and ChapterReaction (emoji allow-list) shipped on the chapter reading page with notification fan-out.

majorworldsApr 25, 2026

Worldbuilding extras: factions, lore quests, cyclical events, maps, timelines

Factions, LoreQuests, CyclicalEvents, Interactive Maps (with editable SVG markers and regions), and a scrubbable per-world Timeline now live under /worlds/[slug]/. New Prisma models for each, mounted as sections on the world page.

majorcardsaspectsabilitiesparserengineApr 25, 2026

Aspects ability DSL: parser, resolver, engine wiring

Card ability text is now parsed into a structured DSL covering triggers (onPlay/onAttack/onTurnStart/...), effects (damage/heal/destroy/bounce/silence/draw/...), conditions, and targets. Match endpoint dispatches at every event hook. Cards that fail to parse get an "unresolved" advisory badge until rewritten.

majorcardsaspectsApr 25, 2026

Aspects card game v1: schema, propose, vote, packs

Six card types (UNIT, ACTION, ANSWER, FIELD, TERRAIN, SIGNATURE) and six aspect slots A-F. New Card / CardSchema / CardVote / UserCardStats / CardInventoryItem / CardPack models. Players can propose cards at /cards/propose, vote at /cards/vote (3/day free, 100/day subs), open packs with transparent odds + pity, and browse per-world card pools.

majortabletopcombatdndApr 24, 2026

Tabletop RPG engine: campaigns, sessions, dice, sheets

Subscriber-only in-platform VTT. New Prisma models: Campaign, CampaignMember, CampaignSession, DiceRoll, CharacterSheet. Worldwright tier gets one active campaign; Loremaster tier can have six (one active). Play happens at /play/[campaignId] with scene panel, dice roller, initiative tracker, and party list.

majorauthApr 24, 2026

Free-tier caps now enforced at the API layer

Free accounts: max 2 stories, 5 chapters per story, 10,000 words per chapter, 5 storyboard panels, 5 characters, 15 lore entries, and 5 short notes per story. Limits return a 402 with requiresSubscription:true so the UI can prompt to upgrade. Subscribers remain unlimited.

minorworldsstoriesuxApr 24, 2026

Minor bug fixes

minorworldsstoriesApr 24, 2026

Minor bug fixes

majorworldsstoriesdbApr 24, 2026

Worlds + galaxies linked to characters and lore entries

StoryCharacter and StoryEntity now carry optional foreign keys to Planet (world) and Galaxy. Editors get a dropdown to attach an existing world/galaxy or create a new one inline. Creating a world is immediate; new galaxies require admin approval through the existing /admin/galaxies queue.