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>
* 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>
* 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>
- 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>
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>
- 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>
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>
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>
- 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>
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>
* 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>
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>
- 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>
* 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>
* 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>
- 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>
- 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>
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>
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>
- 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>
- 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>
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>
* 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>
* 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>
* 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>
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>
* 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>
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>
* 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>
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>
* 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>
* 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>
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>
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>
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>
* 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>
- 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>
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>
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>
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>
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>
- 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>
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>
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>
* 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>
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>
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>
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>
* 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>
Admin server and Eagle run in separate containers in production, so the
admin server cannot directly access Eagle's save directory. Added a streaming
gRPC endpoint DownloadGameSave that zips and streams the save directory,
with the admin server proxying the download through this endpoint.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix JWT claims to match Eagle's expected format
Go auth service was using custom claim names (userId, displayName, isAdmin)
but Eagle's JwtServiceImpl expects standard claims:
- sub (Subject) for userId
- name for displayName
- admin for isAdmin
This fixes "User not found" error when client calls SetDisplayName after
OAuth login, because Eagle couldn't extract the userId from the JWT.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix jwt_test.go to use updated claim field names
Update tests to use Subject instead of UserID and Name instead of
DisplayName, matching the changes to EagleClaims and RefreshClaims.
🤖 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>
- Add ImportGame RPC to eagle.proto for registering uploaded games
- Implement importGame in GamesManager to load saves from disk
- Add download handler: zips game save folder and serves as download
- Add upload handler: accepts zip, extracts, calls gRPC to register
- Add "Download Save" button to game detail page
- Add upload form (collapsible) to games list page
Uploaded games start with all AI players; admin can assign humans
using existing player management features.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Production code:
- Remove LegacyCommandChooser trait and all legacy methods from CommandChooser.scala
- Remove all proto overloads from CommandChoiceHelpers.scala (~1100 lines removed)
- Remove proto imports and converter dependencies
- Rename chosenFulfillEasyQuestsCommandProtoless to chosenFulfillEasyQuestsCommand
- Update MidGameAIClient to use renamed method
- Remove unused CommandChooserImplicits import from AIClient
Test code:
- Update CommandChoiceHelpersTest to use Scala model types
- Update FulfillQuestsCommandSelectorTest to use Scala model types
- Update PerformVassalCommandsPhaseActionTest to use Scala model types
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The /oauth/callback endpoint should proxy to auth:8080 (Go auth service)
not eagle:8080. This fixes 502 errors during OAuth flow.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Implements a Scala client for the Hetzner Cloud API to manage
on-demand Shardok instances for tactical combat. Features:
- Create/delete servers with cloud-init user data
- Get server status and list servers by label
- Power on/off and graceful shutdown operations
- Async execution with Futures
- Proper error handling for API errors
Also updates the plan doc to track ARM64 build completion.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add JWT_PRIVATE_KEY to auth_build.yml deploy job so the auth service
can bootstrap its RSA keys from the JWK secret. This ensures the
JWT key is properly deployed without manual server intervention.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The nimbus-jose-jwt library may output standard base64 (with +/)
instead of base64url (with -_). Try multiple decode strategies:
1. base64url without padding (JWK spec)
2. base64url with padding
3. standard base64 without padding
4. standard base64 with padding
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add ARM64 Linux build infrastructure for Shardok
Infrastructure to support cross-compiling Shardok for ARM64 Linux,
enabling deployment to Hetzner ARM instances (CAX41) for on-demand
compute.
Changes:
- Add linux_arm64 platform definition
- Add LLVM toolchain for ARM64 cross-compilation
- Add ARM64 sysroot placeholder (needs workflow run to populate)
- Add ARM64 busybox for container health checks
- Add Ubuntu 24.04 ARM64 base image
- Add Shardok ARM64 container image targets
- Update sysroot build workflow to support ARM64
Next steps:
1. Run "Build Linux Sysroot" workflow with architecture=arm64
2. Update MODULE.bazel with generated sysroot SHA
3. Build and push ARM64 container
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update ARM64 sysroot SHA from workflow build
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix ARM64 container build issues
- Use linux/arm64/v8 platform string (rules_oci requires variant suffix)
- Remove busybox_layer_arm64 due to busybox.net SSL certificate issues
- Health checks can be added later using an alternative busybox source
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add GitHub Actions workflow for ARM64 Shardok build
Builds and pushes ARM64 container image to GitHub Container Registry
for deployment on Hetzner ARM instances (CAX41).
Triggered on:
- Push to main (when Shardok-related files change)
- Manual workflow_dispatch
🤖 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>
Remove direct port 40033 binding from auth service since nginx
now proxies this port (PR #4987). Both can't bind to the same
host port.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add bootstrapKeysFromJWK() to convert JWK to PEM files on first run
- Uses only Go stdlib (no third-party JWK libraries)
- Pass JWT_PRIVATE_KEY env var to auth service in docker-compose
- PEM files persist in jwt-keys volume after first bootstrap
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Remove proto GameState overloads from 8 AI files, keeping only Scala model versions
- Update all AI tests to use Scala model types (GameState, FactionC, ProvinceC, HeroC)
- Clean up BUILD.bazel deps to remove proto dependencies
- Net reduction: -489 lines of code
Files converted:
- AIClientUtils.scala
- EarlyGameAIClient.scala
- FactionLeaderProvinceRanker.scala
- FixLeaderAloneCommandSelector.scala
- InvitationCommandSelector.scala
- MoveLeaderToBetterProvinceCommandChooser.scala
- ResolveDiplomacyCommandSelector.scala
- All corresponding test files
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Documents the architecture for running Shardok on Hetzner ARM instances
with on-demand spin-up based on player activity:
- Hetzner CAX41 (16 ARM cores) in Ashburn at ~$0.04/hr
- Activity-based spin-up: start when players connect, stop after 1hr idle
- TLS + token authentication (Let's Encrypt for certs, auto-renewed)
- Estimated cost: $2-7/month vs $20-40/month for equivalent always-on
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Deproto MidGameAIClient: flip main entry point to protoless
Completes the deproto migration of MidGameAIClient by:
- Adding protoless chosenMidGameStrategicCommand
- Flipping the main entry point so proto delegates to protoless
- Adding protoless maybeMoveToRecruitCommand to CommandChoiceHelpers
- Adding protoless chosenFulfillEasyQuestsCommandProtoless
- Adding FactionUtils.neutralNeighbors helper
- Fixing test GameState constructors to include currentPhase
The proto versions now delegate to protoless versions via
GameStateConverter.fromProto(), eliminating proto usage in the
main command selection logic.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove proto deps from MidGameAIClient and make test protoless
- Remove all protobuf dependencies from MidGameAIClient BUILD.bazel
- Delete legacy proto-based methods from MidGameAIClient.scala (now fully protoless)
- Convert MidGameAIClientTest to construct Scala GameState directly
- Add helper methods for creating test fixtures (makeGameState, makeFaction, etc.)
- Add visibility for battalion/concrete and state packages for test access
🤖 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>
Uses dynamic DNS resolution to avoid startup dependency on auth container.
Previous version failed because static upstreams resolve at nginx startup.
Changes:
- nginx.conf: Add server block on port 40033 with variable-based grpc_pass
- nginx.conf: Add /health endpoint on port 40033
- docker-compose: Expose 40033 on nginx container
Key fix: Using `set $auth_backend "auth:40033"; grpc_pass grpc://$auth_backend;`
instead of static upstream, so DNS resolution happens at request time
(cached by resolver for 10s) rather than at nginx startup.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds nginx server block to listen on port 40033 and route gRPC
traffic to the Go auth service. This enables Phase 2 clients
to connect directly to the auth service.
Changes:
- nginx.conf: Add auth_grpc upstream and server block on port 40033
- docker-compose: Expose 40033 on nginx, add auth dependency
This is safe to deploy before Phase 2 clients - old clients still
use the Eagle proxy through port 443.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Auth service now has independent deployment lifecycle:
- Only triggers on authservice/** or auth proto changes
- Deploys only the auth container (no Eagle restart)
- Preserves in-memory OAuth state during Eagle deploys
docker_build.yml changes:
- Remove build-auth job
- Preserve existing AUTH_IMAGE in .env
- Only force-recreate eagle, shardok, admin, jfr-sidecar
- Auth container starts but isn't force-recreated
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The deployment was failing because AUTH_IMAGE wasn't being built or
passed to the deploy script. This adds:
- build-auth job to build and push the Go auth service image
- AUTH_IMAGE to deploy job dependencies and env vars
- Auth image pulling in deploy script
- GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET env vars
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Extract OAuth to Go service (Phase 1)
Move OAuth authentication handling from Eagle Scala server into a separate Go
service running in its own container. This simplifies Eagle, enables independent
deployment, and sets up for future JWT validation extraction.
Architecture:
- Go auth service handles OAuth flows, JWT creation, state management
- Eagle proxies Auth gRPC calls to Go service when configured
- Go service calls Eagle's InternalUserService for user persistence
- Shared JWT keys via volume mount
New files:
- src/main/go/net/eagle0/authservice/ - Go auth service
- auth_internal.proto - Internal gRPC for Go→Eagle communication
- InternalUserServiceImpl.scala - User service wrapper for internal gRPC
- ExternalAuthClient.scala - Client for forwarding to Go service
Backward compatible: Eagle runs in standalone mode without --auth-service-url
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add unit tests for Go auth service
- jwt_test.go: Tests for JWT token creation, validation, and refresh
- oauth_test.go: Tests for OAuth state management and status checking
🤖 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>
- Add protoless validateNoFactionLeaderAlone using FactionUtils
- Add protoless provincesWithFactionLeader using FactionUtils and ProvinceUtils
- Add protoless foodSurplus using ProvinceUtils.monthlyFoodSurplus
- Add protoless maybeChosenRaiseSupportCommand (removes fromProto calls)
- Add protoless chosenAttackCommandWithReconInfo using Scala BattalionView
- Proto versions now delegate to protoless versions via fromProto conversion
- Update BUILD.bazel visibility for BattalionViewConverter and BattalionView
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless maybeChosenRelaySuppliesCommand using FoodConsumptionUtils
and ProvinceUtils instead of Legacy versions
- Add protoless selectedReconCommand and maybeChosenReconCommand using
Scala Date extensions and FactionRelationshipC
- Update chosenMidGameCommand to use protoless versions via fromProto conversion
- Add required imports and BUILD.bazel dependencies
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Update OAUTH_NEXT_STEPS.md with current status and remaining work
- Mark completed items (headshots, logout, lobby display, etc.)
- Add new issues discovered (intermittent expired errors, token expiry bug)
- Reorganize implementation plan into Phase 2 (remaining) and Phase 3 (nice-to-haves)
- Update status of all known issues
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up lobby UI elements in Unity scene
- Connect logout button
- Connect lobbyEnvironmentText and lobbyUserText fields
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add lobby environment and user display fields to ConnectionHandler
- Add lobbyEnvironmentText and lobbyUserText fields
- Add UpdateLobbyStatusDisplays() to populate them when entering lobby
- Shows OAuth DisplayName or classic login username
🤖 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>
- Add protoless helper methods in CommandChoiceHelpers:
- isInterestingUHC: uses Scala UnaffiliatedHeroT and RecruitmentInfo
- hasInterestingUHC: uses Scala GameStateC
- maybeChosenTravelToRecruitCommand: protoless overload
- Update MidGameAIClient to use protoless version via fromProto conversion
- Import RecruitmentInfo and UnaffiliatedHeroT for protoless checks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add diagnostic logging to OAuth flow for debugging expired errors
Adds detailed logging to trace OAuth state through the flow:
- getAuthUrl: logs state creation and map sizes
- checkStatus: logs non-pending results with map state
- handleCallback: logs entry, success, and error cases
This will help diagnose why clients sometimes get 'expired' errors
even when the OAuth callback succeeds on the server.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix expiresAt to return access token expiry instead of refresh token expiry
The CheckOAuthStatusResponse.expiresAt field was returning the refresh
token expiry (30 days) but clients interpret this as the access token
expiry (7 days). This fix calculates the correct access token expiry.
🤖 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>
- Add protoless overload that takes native GameState
- Rename proto GameState import to GameStateProto for clarity
- Update AIClient to use protoless version directly
- Remove unused GameStateConverter import from AIClient
- Update BUILD.bazel dependencies
This continues the deproto migration by moving the toProto() conversion
from AIClient into MidGameAIClient, making the public API protoless.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add a logoutButton field that can be wired in Unity, along with
OnLogoutClicked handler that:
- Clears OAuth tokens via OAuthManager.LogoutAsync()
- Disposes gRPC connection and HTTP client
- Returns to connection screen
- Re-shows appropriate auth panel
This allows OAuth users who are auto-logged in to return to the
connection screen to use a different account or auth method.
Note: The button still needs to be added in Unity and wired to
the logoutButton field.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Change chooseFrom to accept Scala GameState instead of proto
- Use protoless CommandChooser with protoless helper methods
- Update chooseMidGameCommandFrom to accept Scala GameState
- Move toProto() conversion to MidGameAIClient call only (mid-game path)
- Use protoless EarlyGameAIClient methods (early game path)
This eliminates the unconditional toProto() conversion in chooseCommand,
now only converting when needed for MidGameAIClient which still uses proto.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless version of resolveDiplomacySelectedCommand using
CommandChooser instead of LegacyCommandChooser
- Add protoless versions of all private helper methods that use
Scala GameState and extract factions/provinces from it
- Rename proto GameState import to GameStateProto to disambiguate
- For methods that don't use gameState (ransom, break alliance),
delegate to Core implementations to avoid code duplication
This enables callers to use Scala GameState directly without
conversion, preparing for elimination of toProto() call in
AIClient.chooseFrom.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add null check for cells array in SetUnitInfoLabels, matching
the pattern used in other methods like ClearCellLabels.
This fixes a race condition where model updates arrive before
the hex grid is fully initialized on the first battle.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Switch from server-mediated headshot fetching (eagle0.net with auth)
to direct CDN access (eagle0-headshots.sfo3.cdn.digitaloceanspaces.com).
- No auth required (bucket is public)
- No server-side changes needed
- Works identically for QA, prod, and local testing
- Removes coupling between headshot fetching and game server
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless overloads for handleCapturedHeroesSelectedCommand,
resolvePleaseRecruitMeSelectedCommand, and freeForAllDecisionSelectedCommand
- These methods don't actually use gameState, so the proto versions now
delegate to core implementations that don't take gameState
- Simplify freeForAllDecisionSelectedCommand to not use CommandChooser
since the inner methods don't use gameState
This prepares the groundwork for eliminating toProto() calls in AIClient.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless overloads for helper methods:
- maybeChosenFeastCommand, maybeChosenGiftCommand
- maybeSpreadIntoOwnedProvince, maybeSpreadIntoEmptyProvince
- maybeChosenGetUnderHeroCapCommand
- maybeChosenAttackToSaveLeaderCommand
- maybeRansomLeaderCommand, chosenRescueLeaderCommand
- Update protoless overloads to use native implementations:
- chosenLoyaltyManagementCommand now uses CommandChooser directly
- chosenRescueLeaderIfAllPrisonersCommand uses leaderIds and
UnaffiliatedHeroType.Prisoner instead of proto equivalents
- Add quest dependency to BUILD.bazel (required for UnaffiliatedHeroT)
This eliminates all GameStateConverter.toProto() calls from
CommandChoiceHelpers.scala protoless overloads.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix JWT auth to set legacy userName for backwards compatibility
When using JWT authentication, contextWithJwtClaims was not setting
the legacy userNameCtxKey, causing AuthorizationUtils.userName to
return null. This broke game management code that relies on userName
for mapping users to factions.
Fix by also setting userNameCtxKey to displayName when using JWT auth,
ensuring backwards compatibility with existing game management code.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add OAuth implementation next steps and design doc
Comprehensive design document covering:
- Known issues (identity fragility, headshots, logout, uniqueness)
- Proposed user identity model with userId as stable key
- Multi-provider account linking strategy
- Avatar/headshot strategy
- Phased implementation plan
- Technical debt and open questions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update OAuth design doc with headshot investigation
- Clarify that headshots are AI-generated character portraits, not user avatars
- Document headshot architecture: client → eagle0.net (home Mac) → S3 signed URL
- Explain why OAuth breaks headshots: eagle0.net nginx only validates Basic Auth
- Clarify PR #4964 is required now (fixes admin server NPE crash)
- Phase 2 migration to userId-based identity deferred
🤖 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>
- Add protoless chosenBuyFoodCommand using protoless foodAmountToBuy
- Add protoless maybeChosenDivineCommand using HeroUtils.power
- Add protoless maybeChosenArmTroopsCommand using ProvinceUtils
- Add protoless maybeChosenRecruitCommand and chosenReturnCommand (impl helpers)
- Convert chosenCommandWhileTraveling to native protoless implementation
using CommandChooser with protoless choosers
This eliminates wasteful GameStateConverter.toProto() conversions when
EarlyGameAIClient calls chosenCommandWhileTraveling with Scala GameState.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless isEarlyGame(GameState, FactionId) using FactionUtils and ProvinceUtils
- Add protoless chooseEarlyGameCommand using CommandChooser with protoless choosers
- Update BUILD.bazel with required deps (FactionUtils, ProvinceUtils, GameState)
- Add exports for GameState to support downstream callers
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The userId was being read inside the Future block, which runs on a
thread from ExecutionContext.global. However, gRPC Context.current
returns the context for the current thread, not the gRPC handler
thread where the JWT claims were attached.
This caused setDisplayName and getCurrentUser to always get null/empty
userId, resulting in "User not found" errors after successful OAuth.
Fix by capturing userId before creating the Future, on the gRPC thread
where Context.current has the proper claims attached. This matches
the pattern used in EagleServiceImpl.lockAndDoWithUserName.
Also update AuthorizationInterceptor publicEndpoints to use
CheckOAuthStatus instead of the old ExchangeCode method name.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless overloads for heroCount and troopCount methods
- Protoless versions take Map[BattalionId, BattalionT] instead of GameState
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Client changes for server-mediated OAuth:
- AuthClient: Use PollForOAuthCompletionAsync instead of deep links
- OAuthManager: Remove deep link handling, use polling instead
The client now:
1. Calls GetOAuthUrl to get browser URL and state token
2. Opens browser for user to authorize
3. Polls CheckOAuthStatus every 2s until success/failure/timeout
Works in Unity Editor and all platforms without URL scheme registration.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless extraHeroCount and desiredHeroCount to AIClientUtils
- Add protoless truceOfferAcceptanceChance to ResolveDiplomacyCommandSelector
- Add protoless allianceOfferAcceptanceChance to ResolveDiplomacyCommandSelector
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Previously image tags were only passed via ssh-action envs, which meant
manual docker-compose restarts would fall back to :latest. Now the SHA-tagged
image names are persisted in .env.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless overloads to FixLeaderAloneCommandSelector
- Add protoless overloads to InvitationCommandSelector
- Add protoless overloads to MoveLeaderToBetterProvinceCommandChooser
- Add protoless overloads to FactionLeaderProvinceRanker
- Add hostileNeighbors helper to FactionUtils for protoless use
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add location block for /net.eagle0.eagle.api.auth.Auth gRPC service
- Add location block for /oauth/callback HTTP endpoint
These routes are required for OAuth login to work.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Convert protoless overloads to use Vector[CommandChooser] directly instead
of delegating to legacy implementations with proto conversion.
Changes:
- chosenExpandCommand: now uses Scala GameState directly
- chosenEntrustCommand: now uses Scala GameState directly
- Added protoless overload for chosenMobilizeIfAdjacentEnemyCommand
This completes the protoless conversion for all main command chooser
entry points (chosenUniversalCommand, chosenDevelopCommand,
chosenMobilizeCommand, chosenExpandCommand, chosenEntrustCommand).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Convert protoless overloads to use Vector[CommandChooser] directly instead
of delegating to legacy implementations with proto conversion.
Changes:
- chosenDevelopCommand: now uses Scala GameState directly
- chosenMobilizeCommand: now uses Scala GameState directly
- Added protoless overloads for helper methods:
- excessBeyondMaximumSupplies
- chosenShipExcessSuppliesCommand
- chosenImproveInfrastructureIfBelowTrainingCommand
- maybeChosenTravelToArmTroopsCommand
- chosenTrainCommand
- suppliesToSend
- preferredSuppliesDestination
- chosenSendSuppliesCommandWithSuppliesAndDestination
- chosenSendSuppliesCommandWithSupplies
- maybeChosenSendSuppliesCommand
This eliminates Scala→proto→Scala conversion roundtrips for callers
that already have a Scala GameState.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>