Commit Graph
9233 Commits
Author SHA1 Message Date
66efefc8d6 Replace Hetzner plan doc with latency hiding strategies (#5053)
The Hetzner on-demand compute implementation is complete:
- ARM64 builds working in CI
- Shardok running on Hetzner CAX41 in Helsinki
- Production Eagle connected via TLS + token auth
- IPv6/NAT64 networking configured

Delete the completed planning doc and add a new doc covering
future latency optimization strategies:
1. Client-side animation masking (recommended first step)
2. Split Shardok architecture (future if needed)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 13:37:53 -08:00
811de67c40 Move Custom Battle to lobby with OAuth authentication (#5051)
* Move Custom Battle to lobby, use OAuth authentication

- Remove _createConnection() from _internalCustomBattle() since connection
  already exists when called from lobby
- Hide gameSelectionPanel instead of connectionPanel when entering custom battle
- Add customBattleButton field for lobby UI button
- Add cancelCustomBattleButton and CancelCustomBattle() to return to lobby
- Wire up button click handlers in SetupLobbyUI()

The Custom Battle button should now be placed in the gameSelectionPanel (lobby)
in Unity and assigned to the customBattleButton field. A cancel button in the
customBattlePanel should be assigned to cancelCustomBattleButton.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Wire up Custom Battle and Cancel buttons in Unity scene

- Assign customBattleButton in lobby panel
- Assign cancelCustomBattleButton in custom battle panel

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix CancelCustomBattle to properly reset canvas states

Hide shardokCanvas and ensure connectionCanvas is visible when
returning to lobby from custom battle.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix Back button onClick handler in custom battle panel

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 13:32:34 -08:00
f4aa19119b Remove dead execute method from ProtolessRandomSequentialResultsAction (#5052)
* Update DEPROTO_PLAN.md: 100% action files now protoless

- ResolveBattleAction migrated to Scala types (PR #5048)
- All 52 action files are now fully protoless
- CommandChoiceHelpers fully migrated to Scala types
- Update progress summary and validation checklist

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Remove dead execute method from ProtolessRandomSequentialResultsAction

The execute(startingState: GameStateProto, applier: ActionResultProtoApplier) method
was never called - all actions using this base class now call .results() directly
and apply results via RandomStateSequencer or ActionResultApplier.

This removes the dead method and its unused dependencies (SeededRandom,
ActionResultProtoApplier, VigorXPApplier, ActionResultProtoConverter).

Note: The proto GameState export is retained because downstream actions use
ProvinceViewFilter which has overloaded methods taking both proto and Scala
GameState - overload resolution requires both types to be visible.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 13:21:08 -08:00
80023d39a0 Update DEPROTO_PLAN.md: 100% action files now protoless (#5050)
- ResolveBattleAction migrated to Scala types (PR #5048)
- All 52 action files are now fully protoless
- CommandChoiceHelpers fully migrated to Scala types
- Update progress summary and validation checklist

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 13:20:46 -08:00
ecffeb0d2c Fix Docker IPv6 check: don't require sudo (#5049)
The deploy user doesn't have passwordless sudo. Change from trying
to configure Docker (which fails) to just checking and warning.

Docker IPv6 is a one-time server setup done manually.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 13:03:28 -08:00
1936d65bea Migrate ResolveBattleAction to Scala types (#5048)
- Change ResolveBattleAction to take Scala GameState and use ActionResultApplier
- Add resolvedBattle field to ActionResultT, ActionResultC, and proto converter
- Update EngineImpl.resolveBattle to use Scala-based applier
- Update test to convert proto GameState to Scala and handle Scala results

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 13:02:28 -08:00
ea46bb4a90 Enable Docker IPv6 for connecting to Hetzner Shardok (#5047)
The Hetzner Shardok server is IPv6-only. Docker containers need
IPv6 support to reach it.

Changes:
- docker-compose.prod.yml: Add IPv6-enabled network
- docker_build.yml: Configure Docker daemon for IPv6 on deploy
  (with ip6tables for NAT)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 12:09:58 -08:00
480ae0d0c6 Fix auth service overwriting external user database changes (#5046)
The auth service loads users.pb into memory at startup. When authcli
modifies the file on disk (e.g., to grant admin), the service still has
the old in-memory copy. On next login, it would overwrite the file with
stale data, losing the admin grant.

Fix: Check file modification time before each FindOrCreateUser call.
If the file was modified externally, reload it before proceeding.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 11:06:44 -08:00
14def8899d Migrate ChronicleEventGenerator to use Scala NotificationDetails (#5045)
- Replace proto notification detail imports with Scala NotificationDetails
- Use NotificationConverter.fromProto to convert proto -> Scala
- Remove dependency on action_result_notification_details_scala_proto
- Add dependencies on notification_trait and notification_converter

The internal pattern matching now uses Scala sealed trait NotificationDetails
instead of proto case classes, reducing proto coupling in the action layer.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 11:05:43 -08:00
adminandClaude Opus 4.5 581ffb735e Make Shardok address configurable via SHARDOK_ADDRESS env var
Allows switching between local Shardok (default: shardok:40042) and
remote Hetzner Shardok (shardok.prod.eagle0.net:40042) via GitHub
Actions secrets.

To use Hetzner Shardok in production:
1. Add secret SHARDOK_ADDRESS=shardok.prod.eagle0.net:40042
2. Add secret SHARDOK_AUTH_TOKEN=<your-token>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 10:51:52 -08:00
74383ae5d9 Fix admin console OAuth flow and route protection (#5044)
* Fix admin console OAuth flow and route protection

Changes:
- Gate all admin routes with requireAuth (games, settings, JFR, APIs)
  Only login, logout, health, and static files are public now
- Add return_url to OAuth flow so auth service redirects back to admin
  console after callback, instead of showing "close this window"
- handleLoginComplete now immediately completes login on redirect
- Update authcli help text with production usage examples

The OAuth flow now works properly for web clients:
1. User clicks Sign in -> admin console redirects to OAuth provider
2. OAuth provider calls auth service callback
3. Auth service redirects back to admin console's /login/complete
4. Admin console sets JWT cookie and redirects to /games

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix oauth_test.go for GetAuthURL signature change

Update test calls to pass empty string for the new returnURL parameter.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 10:51:23 -08:00
ba9c92222c Add asset licensing audit document (#5042)
Document all media assets in Unity project for licensing review before
potentially opening public access to game downloads. Identifies:
- Asset Store purchases (properly licensed)
- CC-licensed music with attribution
- Items needing verification (Shardok sounds, StrategyGameIcons, etc.)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 10:47:28 -08:00
f24c1209e0 Convert ProvinceEvent from sealed trait to Scala 3 enum (#5043)
- Replace sealed trait + case class pattern with Scala 3 enum
- Update all import sites to use `ProvinceEvent.{BeastsEvent, ...}` pattern
- More idiomatic Scala 3 with cleaner syntax

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 10:23:55 -08:00
e7c8df8b33 Add admin user management to admin console (#5041)
* Add admin user management to admin console

Adds functionality to view and manage user identity→display name links:

Backend (auth service):
- New admin.proto with AdminService gRPC (ListUsers, SetUserDisplayName, SetUserAdmin)
- AdminHandler validates JWT is_admin claim for authorization
- UserService methods for listing users and admin operations

Frontend (admin console):
- OAuth login flow with Discord/Google (JWT stored in HTTP-only cookie)
- Auth middleware requiring is_admin claim for /users routes
- Users page with search, display name editing, and admin toggle
- HTMX-powered table with infinite scroll pagination

Deployment:
- admin container now connects to auth service for user management

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add authcli tool for user management

CLI tool for managing auth service users directly via the users.pb file.
Useful for bootstrapping the first admin user or emergency access.

Commands:
- list: List all users with their admin status
- find <email>: Find user by email (partial match)
- set-admin <id> true|false: Set admin status by user ID
- grant-admin <email>: Grant admin to user by exact email match

Usage:
  authcli --data-dir=/app/data list
  authcli --data-dir=/app/data grant-admin your@email.com

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add authcli to auth server Docker image

- Include authcli_linux_amd64 in auth_binary_layer
- Update auth_build.yml paths to trigger on authcli and admin proto changes

Usage on VM:
  docker exec auth-server /app/authcli_linux_amd64 --data-dir=/app/data list
  docker exec auth-server /app/authcli_linux_amd64 --data-dir=/app/data grant-admin user@email.com

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 07:55:23 -08:00
c753c4995a Add Scala overloads to ProvinceEventUtils (#5040)
* Add Scala overloads to ProvinceEventUtils

- Add Scala overloads for isBlizzardEvent, isBeastsEvent, isEpidemicEvent,
  isFestivalEvent, isDroughtEvent, isFloodEvent, isImminentRiotEvent
- Add Scala overloads for beastsCount and beastInfo
- Update BUILD.bazel with required Scala model dependencies
- Add province event dependency to LegacyProvinceUtils for overload resolution

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Replace isInstanceOf with pattern matching in ProvinceEventUtils

- Use match expressions instead of isInstanceOf for type checks
- More idiomatic Scala style

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 07:42:02 -08:00
51e9e9b14a Add Scala overload to ExpandedCombatUnitUtils (#5039)
- Add protoless overload that takes ScalaGameState and CombatUnitC
- Uses BattalionViewFilter (protoless) and BattalionViewConverter for output
- Exports CombatUnitC and ScalaGameState types for downstream callers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 06:00:24 -08:00
a2feaa6546 Add protoless Scala overloads to diplomacy Resolve command factories (#5038)
- EligibleDiplomacyStatuses: Add Scala overload for maybeImprisonStatus
  that takes provinces Iterable instead of proto GameState
- AvailableResolveTruceOfferCommandFactory: Add Scala overload that
  filters Scala TruceOffer instances and converts to proto output
- AvailableResolveAllianceOfferCommandFactory: Add Scala overload for
  AllianceOffer filtering
- AvailableResolveBreakAllianceCommandFactory: Add Scala overload for
  BreakAlliance filtering
- AvailableResolveInvitationCommandFactory: Add Scala overload with
  protoless invitation validation using FactionUtils
- AvailableResolveRansomOfferCommandFactory: Add Scala overload with
  protoless RansomValidity and conflict checking

All Scala overloads take ScalaGameState and output proto commands,
allowing callers with Scala model types to avoid GameStateConverter.toProto().

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 22:49:44 -08:00
4d83978951 Fix OAuth reconnection failure due to PlayerPrefs threading (#5037)
TokenStorage was calling PlayerPrefs.GetString() which can only be
called from Unity's main thread. When PersistentClientConnection
attempts to reconnect from a background thread, JwtAuthInterceptor
tries to get the access token, causing a UnityException.

This caused an infinite loop: DeadlineExceeded → reconnect attempt →
PlayerPrefs exception → reconnect fails → idle timeout → retry...

Changes:
- TokenStorage: Add in-memory cache for thread-safe token access
- OAuthManager: Initialize cache on main thread in Awake()
- PersistentClientConnection: Schedule reconnect when stream ends
  normally (was missing, causing silent connection death)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 22:38:03 -08:00
11de6872d4 Add TLS support to ExternalAuthClient for remote auth connections (#5036)
Enable QA Eagle to connect to production auth service over TLS.

- Use TLS for external connections (non-localhost hosts)
- Keep plaintext for local container-to-container connections
  (localhost, auth, 127.x.x.x)

This allows running QA Eagle with --auth-service-url prod.eagle0.net:40033
to share user accounts with production.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 22:25:50 -08:00
ecd089de59 Add Sentry integration for exception alerting (#5011)
- Add io.sentry:sentry dependency
- Initialize Sentry from SENTRY_DSN environment variable
- Report uncaught exceptions via ExceptionInterceptor
- Add SENTRY_DSN to docker-compose and CI workflow

When SENTRY_DSN is configured, uncaught exceptions in gRPC handlers
will be reported to Sentry for email/Slack alerts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 22:20:05 -08:00
c1756cb36b Add protoless ProvinceViewFilter overload with faction filtering (#5034)
- Add filteredProvinceView(ProvinceT, ScalaGameState, FactionId) overload
- Use protoless FactionUtils.hasAlliance, Visibility.hasFullVisibility, and ProvinceUtils.incomingOthers
- Add helper methods: fullProvinceInfoScala, maybeIncomingAttackersScala, unaffiliatedHeroInfoScala
- Handle reconned provinces directly from Scala FactionT.reconnedProvinces

GameStateViewFilter improvements:
- Eliminate GameStateConverter.toProto() call in Scala overload
- Use new ProvinceViewFilter Scala overload for faction filtering
- Convert battalionTypes and chronicleEntries to proto only at output boundary

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 22:16:44 -08:00
78533a9ff0 Remove Eagle user management - forward to Go auth service (#5033)
Phase 2 of moving user management to Go auth service:
- Eagle now forwards setDisplayName, getCurrentUser, logout to Go auth
- Removed InternalUserServiceImpl and internal gRPC server (port 40034)
- UserService is now optional (only created when not using external auth)
- Updated docker-compose to remove port 40034 exposure

This makes Eagle stateless for user data when configured with
--auth-service-url, enabling QA to point to prod auth service.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 21:39:24 -08:00
8172a4e117 Migrate CheckForFulfilledQuestsAction to protoless BattalionTypeFinder (#5032)
* Migrate CheckForFulfilledQuestsAction to protoless BattalionTypeFinder

- Change battalionTypes parameter from proto Vector[BattalionType] to Scala Vector[BattalionType]
- Update callers (EngineImpl, EndVassalCommandsPhaseAction) to pass Scala types directly
- Eliminate wasteful BattalionTypeConverter.toProto() conversions
- Update test to use Scala BattalionType

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add Scala overload to ExpandedUnaffiliatedHeroUtils, eliminate proto conversions

- Add ExpandedUnaffiliatedHeroUtils.expandedUnaffiliatedHero(ScalaGameState, UnaffiliatedHeroT)
- Add UnaffiliatedHeroConverter.unaffiliatedHeroTypeToProto() helper for efficient enum conversion
- Use reverse map instead of inefficient .find() in UnaffiliatedHeroConverter.toProto()
- Update AvailablePleaseRecruitMeCommandFactory to use Scala overload directly
- Eliminate wasteful GameStateConverter.toProto() and UnaffiliatedHeroConverter.toProto() calls

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add exports to expanded_unaffiliated_hero_utils for transitive type visibility

Callers that import ExpandedUnaffiliatedHeroUtils now see both overloads in
method signatures, which exposes ScalaGameState and UnaffiliatedHeroT types.
Add these as exports so callers can compile without needing explicit deps.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 21:33:32 -08:00
fbabe9cea4 Move user management to Go auth service (Phase 1) (#5030)
* Move user management to Go auth service (Phase 1)

Auth service now manages users locally instead of calling Eagle:
- Add users.go with UserService for protobuf-based user persistence
- Implement SetDisplayName, GetCurrentUser, Logout handlers
- Extract JWT from gRPC metadata (authorization header)
- Add ValidateAccessToken function to jwt.go
- Add user_go_proto target for user.proto
- Add AUTH_DATA_DIR env var and /app/data volume mount

Auth service no longer depends on Eagle's InternalUserService.
Eagle's user management code remains for now (Phase 2 cleanup).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add unit tests and migration path for user service

- Add comprehensive unit tests for UserService (users_test.go)
- Add automatic migration from Eagle's users.pb location
- Add atomic write pattern for crash safety (write .tmp, then rename)
- Add AUTH_LEGACY_DATA_DIR config for migration path
- Mount Eagle's saves volume read-only for migration access

Migration strategy:
1. Auth service checks /app/data/users.pb first
2. If not found, reads from /app/saves/auth/users.pb (Eagle's location)
3. Saves migrated data to new location
4. Subsequent reads/writes use new location only

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 19:21:45 -08:00
7df24149f7 Deproto Phase 5: Document Legacy utility migration status (#5031)
* Add sortKey to FactionUtils to match LegacyFactionUtils API

- Add sortKey method and sortIgnoredChars constant to protoless FactionUtils
- Update LegacyFactionUtils to use direct proto field access (avoid converter
  failures on incomplete test data)
- Both implementations now have matching APIs for the deproto migration
- Update DEPROTO_PLAN.md with progress

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Document Legacy* utility migration status in DEPROTO_PLAN.md

All Legacy* utilities now have protoless equivalents with matching APIs:

**Parallel Implementations (proto mirrors protoless):**
- FactionUtils / LegacyFactionUtils - 24+ boundary callers
- HeroUtils / LegacyHeroUtils - 10 boundary callers
- ProvinceUtils / LegacyProvinceUtils - 20 boundary callers

**Awaiting migration of callers:**
- BattalionUtils / LegacyBattalionUtils - 4 callers
- BattalionViewFilter / LegacyBattalionViewFilter - 3 callers
- BattalionTypeFinder / LegacyBattalionTypeFinder - 2 callers

Legacy versions are appropriately used by boundary code (availability
factories, view filters, action appliers) that works with proto GameState.
The AI layer already uses the protoless versions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 18:41:44 -08:00
7de74539f0 Fix LocalFilePersister to create parent directories (#5029)
When saving to paths like "auth/users.pb", the parent directory
may not exist. FileOutputStream doesn't create parent directories,
so the save would silently fail. Now we ensure parent directories
exist before writing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 17:31:54 -08:00
e9dd53fdf8 Hetzner setup: docs + Step 7 implementation (#5018)
* Update Hetzner docs with actual deployment details

- Use shardok.prod.eagle0.net as the domain name
- Step 4: Specify GitHub Actions secrets location
- Step 5: Recommend Hillsboro, OR (hil) + IPv6 floating IP
- Update all code examples and cloud-init scripts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Wire SHARDOK_AUTH_TOKEN through Eagle's Shardok connection

Implements Step 7 of Hetzner setup guide:
- Main.scala reads SHARDOK_AUTH_TOKEN env var and creates ShardokSecurityConfig
- TLS is auto-enabled when Shardok address contains ".eagle0.net"
- Security config passed to both newGamesManager and newCustomBattleManager
- docker-compose.prod.yml passes SHARDOK_AUTH_TOKEN to Eagle container
- docker_build.yml passes secret during deployment

This is backward compatible: local Docker Shardok (shardok:40042) continues
to work without TLS/auth since the address doesn't contain ".eagle0.net".

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Update ShardokInstanceManager for DigitalOcean registry and TLS

- Update cloud-init script to use DigitalOcean Container Registry instead of ghcr.io
- Add Let's Encrypt/Certbot setup for TLS certificates
- Configure automatic certificate renewal via cron
- Pass TLS cert paths and auth token file to Shardok container
- Default to shardok.prod.eagle0.net domain

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix cloud-init to use eagle0.conf instead of environment variables

The Shardok C++ server reads configuration from /usr/local/share/eagle0/eagle0.conf,
not environment variables. Updated cloud-init script to:
- Create eagle0.conf with TLS and auth paths
- Mount the config directory into the container
- Remove unused -e environment variable flags

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Simplify spin-up strategy: trigger on human turns with 10-min idle timeout

Instead of trying to predict battles, we now:
- Spin up Shardok when any human player takes a turn
- Shut down after 10 minutes of no human turns

This is simpler and more reliable. Battles happen regularly during active
play, so Shardok will be ready when needed. The 10-minute timeout is long
enough to cover thinking time but short enough to minimize idle costs.

Updated:
- SHARDOK_ON_DEMAND_COMPUTE.md with new strategy and state machine
- ShardokInstanceManager: renamed onPlayerActivity -> onHumanTurn,
  changed default idle timeout from 60 to 10 minutes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Document Hetzner testing progress and ARM64 blocker

Testing status:
- Hetzner CAX41 ARM64 server created in Helsinki
- Floating IPv6 configured with netplan persistence
- DNS and Let's Encrypt certificates working
- NAT64 configured for IPv6→IPv4 registry access

BLOCKER: ARM64 container crashes with runfiles error:
"cannot find runfiles (argv0="/app/shardok-server")"
Needs BUILD.bazel investigation for ARM64 image packaging.

Also documented known issues:
- IPv6-only servers need NAT64 DNS (nat64.net)
- Floating IP requires manual netplan config

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix ARM64 container crash: add missing environment variables

The ARM64 Shardok container was crashing with "cannot find runfiles"
because SHARDOK_RESOURCES_PATH and SHARDOK_MAPS_PATH weren't set.
Without these, the binary tries to use Bazel runfiles which don't
exist in the container.

Added the required environment variables to the docker run command
in the cloud-init script, matching docker-compose.prod.yml.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 17:07:10 -08:00
35e8f85627 Fix duplicate UnaffiliatedHero entries when only hero departs (#5028)
When the only ruling faction hero departed from a province, clearRulingFaction
would create duplicate UnaffiliatedHeroC entries - one from the departure logic
and another from clearRulingFaction iterating over all rulingFactionHeroIds.

Fix: Filter out heroIds that already have entries in newUnaffiliatedHeroes
before creating new ones.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 17:04:41 -08:00
35bc242582 Fix upload section styling in admin panel (#5027)
* Fix upload section styling in admin panel

- Add explicit positioning to prevent overlap with other elements
- Style the details/summary for clearer expand/collapse indicators
- Add border and background when expanded for visual clarity
- Prevent button from shrinking in flex layout

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix upload section contrast and button sizing

- Use card background color for summary with proper text contrast
- Change Upload button to btn-small class for appropriate sizing
- Add proper border and styling for expanded form area
- Remove inline styles in favor of CSS classes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix Upload button to not be full-width

- Add width: auto !important to override Pico CSS defaults
- Add flex-grow: 0 to prevent stretching in flex container
- Add position: static to prevent any fixed/absolute positioning
- Add z-index: auto to upload section to prevent stacking issues

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 16:52:45 -08:00
c5829b1aff Remove unused Cloudflare OAuth relay worker (#5026)
This was never successfully deployed. OAuth callbacks are handled
directly by the Go auth service via nginx routing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 16:44:48 -08:00
6c78d9f733 Remove dead code from LegacyHeroUtils and LegacyFactionUtils (#5025)
* Remove dead code from LegacyHeroUtils

- Remove unused `seniorityOrder` method (protoless version in HeroUtils is used)
- Remove unused `sortOrdering` method (protoless version in HeroUtils is used)
- Remove unused `discordance` method (protoless version in HeroUtils is used)
- Remove unused `faction` method (protoless version in HeroUtils is used)
- Remove unused `Faction` import and proto dependency
- Remove corresponding tests for deleted methods

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Remove dead code from LegacyFactionUtils

- Remove unused `trust` method (protoless version in FactionUtils is used)
- Remove unused `factionHead` method
- Remove unused `ownedNeighbors` method (protoless version in FactionUtils is used)
- Remove unused `hostileNeighbors` methods (protoless version in FactionUtils is used)
- Remove unused `neutralNeighbors` method (protoless version in FactionUtils is used)
- Remove unused `truceExpirationDate` method
- Remove unused `alliedFactions` method (protoless version in FactionUtils is used)
- Remove unused `provincesWithHostileNeighbors` method
- Remove unused `ProvinceWithHostileNeighbors` case class
- Remove unused `Hero` import and `hero_scala_proto` dependency
- Remove corresponding tests for deleted methods

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 15:29:57 -08:00
1966dfbc24 Update DEPROTO_PLAN.md and remove dead code from LegacyBattalionUtils (#5022)
* Update DEPROTO_PLAN.md with Phase 5 progress

- Add section for thin wrapper refactorings (LegacyRansomValidity, LegacyRecruitmentOdds)
- Note LegacyHeroUtils dependency on LegacyFactionUtils
- Note LegacyBattalionUtils has intentionally different power multipliers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Remove dead code from LegacyBattalionUtils

- Remove unused `power` method (superseded by BattalionPower.power)
- Remove unused `estimatedPower` method (superseded by BattalionPower.estimatedPower)
- Remove unused `powerMultiplier` map (only used by removed methods)
- Remove unused `BattalionView` import and proto dependency

All callers already use BattalionPower for power calculations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 12:33:03 -08:00
7cd7edf9a0 Fix null reference race in lobby subscriber callbacks (#5023)
The code checked _lobbySubscriber != null before enqueueing a lambda,
but the lambda captured the field reference and executed later on the
main thread when _lobbySubscriber could have become null (e.g., during
Dispose).

Fix by capturing the subscriber in a local variable before the null
check. This ensures the lambda uses the subscriber that was active
when the message arrived, even if _lobbySubscriber changes later.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 12:32:40 -08:00
104ee5429d Add scheduled workflow to cleanup old container images (#5024)
Runs daily at 3am UTC to delete container images older than 5 days from
DigitalOcean Container Registry. Protected tags (latest, arm64-latest)
are never deleted.

Also runs garbage collection after cleanup to reclaim storage.

Includes manual trigger with dry-run option for testing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 12:31:31 -08:00
57fd73571f Refactor LegacyRecruitmentOdds to delegate to protoless RecruitmentOdds (#5021)
- Convert LegacyRecruitmentOdds from duplicated logic to thin wrapper
- Uses FactionConverter, HeroConverter, UnaffiliatedHeroConverter
- Delete LegacyRecruitmentOddsTest (protoless version already tested)
- Reduces duplicate code, centralizes recruitment odds calculation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 11:56:50 -08:00
9ea6e29752 Delete unused Legacy* utilities: LegacyProvinceDistances, LegacyBattalionSuitability, LegacyFoodConsumptionUtils, LegacyHandleRiotUtils (#5020)
Phase 5 of deproto cleanup - remove Legacy* utilities that have no production callers:

- LegacyProvinceDistances: no callers besides its own test
- LegacyBattalionSuitability: no callers besides its own test
- LegacyFoodConsumptionUtils: no callers besides its own test
- LegacyHandleRiotUtils: no callers besides its own test

Updated DEPROTO_PLAN.md to track Phase 5 progress.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 09:49:51 -08:00
8581232f70 Add Scala overloads to BattalionNameFilter and BattleFilter (#5019)
* Eliminate GameState toProto() conversion in LLM pipeline

Phase 1 of deproto optimization: Make LlmRequestWithGameState use Scala
GameState instead of proto GameState, eliminating wasteful round-trip
conversion in UnrequestedTextHandler (the heaviest proto conversion in profiles).

Changes:
- LlmResolver: Change LlmRequestWithGameState.gameState to Scala GameState
- UnrequestedTextHandler: Remove GameStateConverter.toProto() calls, pass
  gameHistory.stateAfter() directly
- Update all prompt generators (~38 files) to use Scala model types:
  - FactionT instead of proto Faction
  - HeroT instead of proto Hero
  - ProvinceT instead of proto Province
  - Scala enums (Gender.Male, Profession.Mage) instead of proto enums
- DivineMessagePromptGenerator: Rewrite quest pattern matching to use
  Scala QuestC types instead of proto quest types
- ChronicleUpdatePromptGenerator: Inline province lookup, remove
  LegacyFactionUtils dependency
- BattalionDescriptions: Add Scala BattalionT and BattalionTypeId overloads
- Update test files to use concrete Scala types (HeroC, FactionC, ProvinceC)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Make AttackCommandChooser fully protoless

- Add `estimatedPower(BattalionViewC)` to `BattalionPower` for handling recon
  data with optional training/armament (defaults to 50.0 when unknown)
- Update `AttackCommandChooser.chosenAttackCommand` to use Scala `BattalionViewC`
  instead of proto `BattalionView`
- Update `MidGameAIClient` to pass battalions directly without proto conversion
- Remove `LegacyBattalionUtils` and proto `battalion_view_scala_proto` dependencies
- Update DEPROTO_PLAN.md: Phase 1 complete, CommandChoiceHelpers fully protoless

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add Scala overloads to BattalionNameFilter and BattleFilter

- Add Scala GameState overload to BattalionNameFilter using FactionUtils.provinces
- Add Scala overload to BattleFilter using FactionUtils.hostilityStatus
- Update GameStateViewFilter Scala overload to use the new Scala sub-filters
- Add shardok_battle visibility for view_filters package
- Update DEPROTO_PLAN.md to reflect completed work

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 09:26:25 -08:00
053ac4a622 Add Scala overloads to view filters (Phase 4 deproto) (#5016)
- Add Scala overloads to Visibility.scala for hasFullVisibility using
  Vector[FactionT] instead of proto GameState
- Add Scala overload to HeroViewFilter.filteredHeroView taking HeroT
  and ScalaGameState with proper type conversions
- Add Scala overload to FactionViewFilter.filteredFactionView taking
  FactionT and ScalaGameState
- Update GameStateViewFilter Scala overload to use Scala sub-filters
  where available (HeroViewFilter, FactionViewFilter)
- Update BUILD.bazel files for visibility and exports

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 09:04:17 -08:00
09041887c8 Fix ARM64 image push: use same repo with arm64- tag prefix (#5012)
DigitalOcean free tier limits to 5 repositories. Use the existing
shardok-server repository with arm64-prefixed tags instead of a
separate shardok-server-arm64 repository.

- shardok-server:latest (x86)
- shardok-server:arm64-latest (ARM64)

Also adds HETZNER_SETUP.md with infrastructure setup instructions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 08:27:02 -08:00
58fd3dc112 Fix user database persistence in Docker (#5017)
The user database was being written to ~/eagle0/eagle/save/ but Docker
only mounted ./saves to /app/saves. This caused user data (including
displayName) to be lost on every container restart.

Add EAGLE_SAVE_DIR and EAGLE_ARCHIVE_DIR environment variables to
SaveDirectory, defaulting to the existing paths for local development.
Docker compose now sets these to /app/saves and /app/archived which
are properly mounted to persistent volumes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 08:26:15 -08:00
d62bb747dd Optimize GetGameHistory to not load entries when only count is needed (#5015)
When the admin panel requests game history with limit=0, it only needs
the total count for pagination. Previously this would load all entries
and serialize each one to JSON, causing timeouts on games with many
actions.

Now when limit <= 0, we return just the count with empty entries,
making the count request fast regardless of history size.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 07:04:16 -08:00
f8646c7b50 Fix game ID parsing in download handler (#5014)
The download handler was using ParseInt which fails for game IDs that
have the high bit set (appear as negative when formatted as signed).
The gameID is already parsed and validated in handleGameRoutes using
ParseUint, so just pass it through instead of re-parsing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 07:03:52 -08:00
affd0a8180 Add Scala GameState overload to GameStateViewFilter (#5010)
- Add `filteredGameState(gs: ScalaGameState, factionId: Option[FactionId])`
  overload that converts internally for now (sub-filters still need proto)
- Update HumanPlayerClientConnectionState to pass Scala GameState directly,
  removing 3 wasteful toProto conversions at call sites
- Add visibility for view_filters to access GameStateConverter
- Export Scala GameState from game_state_view_filter target
- Update DEPROTO_PLAN.md to reflect Phase 4 progress

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 22:32:23 -08:00
a92a878465 Switch ARM64 Shardok image to DigitalOcean Container Registry (#5009)
Use DigitalOcean registry for ARM64 images instead of GitHub Container
Registry for consistency with all other images (Eagle, Shardok x86,
admin, auth, jfr-sidecar).

- Update ci/BUILD.bazel: shardok_server_push_arm64 → registry.digitalocean.com
- Update shardok_arm64_build.yml: use DO_REGISTRY_TOKEN auth
- Update docs to reflect the registry change

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 22:25:17 -08:00
10696325e9 Return new JWT from SetDisplayName with updated displayName claim (#5008)
When a user sets their display name, the current JWT still has the old
(blank) displayName. This caused games to be created with blank usernames.

Fix: SetDisplayName now returns a new access token with the updated
displayName claim. The client stores this new token so subsequent requests
(like joining games) use the correct displayName.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 22:20:00 -08:00
88273937da Add TLS and token authentication to Shardok server (#5001)
* Add TLS and token authentication support to Shardok server

Enables secure communication between Eagle and remote Shardok instances:

- TLS support: Read SSL cert/key from config paths, create SslServerCredentials
- Token auth: Validate "Authorization: Bearer <token>" header on all RPCs
- Config: Added authTokenPath to ServerConfiguration
- TokenValidator class reads token from file and validates requests
- Graceful fallback: If TLS not configured, uses insecure credentials

Configuration options (via eagle0.conf or environment):
- sslCertPath: Path to TLS certificate (e.g., Let's Encrypt fullchain.pem)
- sslPrivateKeyPath: Path to TLS private key
- authTokenPath: Path to file containing the auth token

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Update implementation plan to mark completed tasks

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add TLS and token authentication support to Eagle's Shardok client

Add ShardokSecurityConfig to configure TLS and auth token for connecting
to remote Shardok instances. When useTls is enabled, uses system trust
store for Let's Encrypt certificates. When authToken is set, adds Bearer
token to all gRPC requests via BearerTokenInterceptor.

This enables Eagle to connect securely to Shardok instances running on
Hetzner Cloud with Let's Encrypt TLS and token-based authentication.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 22:12:31 -08:00
e472ab4df2 Add ShardokInstanceManager for on-demand compute lifecycle (#4998)
Manages the lifecycle of Shardok instances on Hetzner Cloud:
- Activity-based spin-up when players connect
- Automatic shutdown after idle timeout (default 60 minutes)
- State machine tracking: Stopped -> Starting -> Ready -> InUse
- Cloud-init script generation for Docker container setup
- Health checking during startup

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 22:10:05 -08:00
11110a5f88 Client connects directly to Go auth service for OAuth (#4982)
Phase 2 of OAuth extraction: Unity client now routes OAuth RPCs
(GetOAuthUrl, CheckOAuthStatus, RefreshToken) directly to the Go
auth service on port 40033, while user RPCs (SetDisplayName,
GetCurrentUser, Logout) continue to go to Eagle.

Changes:
- AuthClient: Use separate gRPC channels for auth service and Eagle
- OAuthManager: Accept both authServiceUrl and eagleUrl parameters
- ConnectionHandler: Pass both URLs when configuring OAuthManager

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 22:06:42 -08:00
d125394028 Fix action history pagination to avoid loading all results (#5005)
Previously, getGameHistory loaded all action results with history.all
then paginated in-memory. For games with many actions, this caused
timeouts.

Now uses history.since(startIndex).take(limit) which efficiently loads
only the save files containing the requested range. The since() method
in PersistedHistory already calculates which partial game files to read.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 22:06:06 -08:00
99037c7c3a Make AttackCommandChooser fully protoless (#5007)
* Eliminate GameState toProto() conversion in LLM pipeline

Phase 1 of deproto optimization: Make LlmRequestWithGameState use Scala
GameState instead of proto GameState, eliminating wasteful round-trip
conversion in UnrequestedTextHandler (the heaviest proto conversion in profiles).

Changes:
- LlmResolver: Change LlmRequestWithGameState.gameState to Scala GameState
- UnrequestedTextHandler: Remove GameStateConverter.toProto() calls, pass
  gameHistory.stateAfter() directly
- Update all prompt generators (~38 files) to use Scala model types:
  - FactionT instead of proto Faction
  - HeroT instead of proto Hero
  - ProvinceT instead of proto Province
  - Scala enums (Gender.Male, Profession.Mage) instead of proto enums
- DivineMessagePromptGenerator: Rewrite quest pattern matching to use
  Scala QuestC types instead of proto quest types
- ChronicleUpdatePromptGenerator: Inline province lookup, remove
  LegacyFactionUtils dependency
- BattalionDescriptions: Add Scala BattalionT and BattalionTypeId overloads
- Update test files to use concrete Scala types (HeroC, FactionC, ProvinceC)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Make AttackCommandChooser fully protoless

- Add `estimatedPower(BattalionViewC)` to `BattalionPower` for handling recon
  data with optional training/armament (defaults to 50.0 when unknown)
- Update `AttackCommandChooser.chosenAttackCommand` to use Scala `BattalionViewC`
  instead of proto `BattalionView`
- Update `MidGameAIClient` to pass battalions directly without proto conversion
- Remove `LegacyBattalionUtils` and proto `battalion_view_scala_proto` dependencies
- Update DEPROTO_PLAN.md: Phase 1 complete, CommandChoiceHelpers fully protoless

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 22:05:22 -08:00